From 76bb7ff2b667dd71f06c6a5c12ea851807b2acee Mon Sep 17 00:00:00 2001 From: Bek <66288351+bek91@users.noreply.github.com> Date: Fri, 21 Aug 2026 01:13:42 -0400 Subject: [PATCH] fix(skills): omit content hashes from prompts (#126951) --- docs/concepts/system-prompt.md | 3 +- src/agents/system-prompt.test.ts | 3 -- src/agents/system-prompt.ts | 2 +- src/skills/loading/local-loader.ts | 2 -- src/skills/loading/session.ts | 3 +- src/skills/loading/skill-contract.test.ts | 16 ++++++---- src/skills/loading/skill-contract.ts | 10 ------- src/skills/loading/skill-version.ts | 6 ---- .../loading/workspace-skill-prompt.test.ts | 10 ++----- .../loading/workspace-skill-snapshot.test.ts | 29 +------------------ src/skills/runtime/remote-skills.ts | 2 -- src/skills/test-support/test-helpers.ts | 2 -- src/skills/types.ts | 2 +- 13 files changed, 18 insertions(+), 72 deletions(-) delete mode 100644 src/skills/loading/skill-version.ts diff --git a/docs/concepts/system-prompt.md b/docs/concepts/system-prompt.md index 053fecdac046..8dc474dcaf78 100644 --- a/docs/concepts/system-prompt.md +++ b/docs/concepts/system-prompt.md @@ -143,7 +143,7 @@ See [Timezones](/concepts/timezone) and [Date & Time](/date-time) for full behav ## Skills -When eligible skills exist, OpenClaw injects a compact `` list (`formatSkillsForPrompt`) with the **file path** and a content-derived `sha256:...` marker per skill. The prompt instructs the model to use `read` to load the SKILL.md at the listed location (workspace, managed, or bundled), and to re-read a skill when its `` differs from a previous turn. If no skills are eligible, the Skills section is omitted. +When eligible skills exist, OpenClaw injects a compact `` list (`formatSkillsForPrompt`) with the **file path** for each skill. The prompt instructs the model to use `read` to load the SKILL.md at the listed location (workspace, managed, or bundled). If no skills are eligible, the Skills section is omitted. Native Codex turns receive this list as turn-scoped collaboration developer instructions instead of per-turn user input, except lightweight cron turns that preserve the exact scheduled prompt. Other harnesses keep the normal prompt section. @@ -157,7 +157,6 @@ Eligibility includes skill metadata gates, runtime environment/config checks, an ... ... ... - sha256:... ``` diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index f3c0ef7bbbf2..f9c59129c017 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -378,7 +378,6 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("## Skills"); expect(prompt).toContain(""); - expect(prompt).toContain("Changed : re-read"); expect(prompt).toContain("External writes: batch safely"); }); @@ -922,7 +921,6 @@ describe("buildAgentSystemPrompt", () => { "Scan . Clear match: read exact with `Read`; obey.", ); expect(prompt).not.toContain("/SKILL.md"); - expect(prompt).toContain("Changed : re-read"); expect(prompt).toContain("Several: most specific"); expect(prompt).toContain("Docs: /tmp/openclaw/docs"); expect(prompt).toContain( @@ -1186,7 +1184,6 @@ describe("buildAgentSystemPrompt", () => { "Scan . Clear match: read exact with `read`; obey.", ); expect(prompt).not.toContain("/SKILL.md"); - expect(prompt).toContain("Changed : re-read"); expect(prompt).toContain("Several: most specific"); }); diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 4b915487f74c..c13b405a9529 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -283,7 +283,7 @@ function buildSkillsSection(params: { params.codeModeActive ? 'Scan . Clear match: use `skills.read("")` inside `exec`; obey.' : `Scan . Clear match: read exact with \`${params.readToolName}\`; obey.`, - "Changed : re-read. Several: most specific. None: read none.", + "Several: most specific. None: read none.", "Up-front max one. Never invent paths.", "External writes: batch safely; no tight loops; honor 429/Retry-After.", trimmed, diff --git a/src/skills/loading/local-loader.ts b/src/skills/loading/local-loader.ts index 496b95ed3e1a..0bdc26df06bc 100644 --- a/src/skills/loading/local-loader.ts +++ b/src/skills/loading/local-loader.ts @@ -13,7 +13,6 @@ import { resolveSkillDisplayName, type Skill, } from "./skill-contract.js"; -import { computeSkillPromptVersion } from "./skill-version.js"; type LoadedLocalSkill = { skill: Skill; @@ -112,7 +111,6 @@ function loadSingleSkillDirectory(params: { description, filePath, baseDir, - promptVersion: computeSkillPromptVersion(raw), source: params.source, sourceInfo: createSyntheticSourceInfo(filePath, { source: params.source, diff --git a/src/skills/loading/session.ts b/src/skills/loading/session.ts index 707783271532..b961d19c2ede 100644 --- a/src/skills/loading/session.ts +++ b/src/skills/loading/session.ts @@ -15,7 +15,6 @@ import { getArchivedSkillFiles } from "../workshop/curator.js"; import { parseSkillFrontmatter, resolveSkillInvocationPolicy } from "./frontmatter.js"; import { resolveSkillDisplayName } from "./skill-contract.js"; import { formatSkillsForPromptBounded } from "./skill-prompt-limits.js"; -import { computeSkillPromptVersion } from "./skill-version.js"; /** Max name length per spec */ const MAX_NAME_LENGTH = 64; @@ -30,6 +29,7 @@ export interface Skill { description: string; filePath: string; baseDir: string; + /** @deprecated Ignored; retained for API compatibility until the next Plugin SDK major. */ promptVersion?: string; source: string; sourceInfo: SourceInfo; @@ -254,7 +254,6 @@ function loadSkillFromFile( description: frontmatter.description, filePath, baseDir: skillDir, - promptVersion: computeSkillPromptVersion(rawContent), source, sourceInfo: createSkillSourceInfo(filePath, skillDir, source), disableModelInvocation: invocation.disableModelInvocation, diff --git a/src/skills/loading/skill-contract.test.ts b/src/skills/loading/skill-contract.test.ts index 15fddc38a06a..258cd082a1b8 100644 --- a/src/skills/loading/skill-contract.test.ts +++ b/src/skills/loading/skill-contract.test.ts @@ -42,7 +42,9 @@ describe("formatSkillsCompact", () => { makeSkill("notes", "Summarize notes", "/tmp/notes/SKILL.md"), { ...makeSkill("weather", "Get weather & forecasts"), promptVersion: "sha256:abc123" }, ]; - expect(formatSkillsForPromptCore(skills)).toBe(upstreamFormatSkillsForPrompt(skills)); + const out = formatSkillsForPromptCore(skills); + expect(out).toBe(upstreamFormatSkillsForPrompt(skills)); + expect(out).not.toContain(""); }); it("renders all passed skills in the full formatter without reapplying visibility policy", () => { @@ -56,14 +58,16 @@ describe("formatSkillsCompact", () => { expect(formatSkillsCompact([])).toBe(""); }); - it("keeps compact descriptions with name, location, and version", () => { - const out = formatSkillsCompact([ - { ...makeSkill("weather", "Get weather data"), promptVersion: "sha256:abc123" }, - ]); + it("keeps compact descriptions with name and location", () => { + const skill = { + ...makeSkill("weather", "Get weather data"), + promptVersion: "sha256:abc123", + }; + const out = formatSkillsCompact([skill]); expect(out).toContain("weather"); expect(out).toContain("Get weather data"); expect(out).toContain("/skills/weather/SKILL.md"); - expect(out).toContain("sha256:abc123"); + expect(out).not.toContain(""); }); it("omits descriptions when their compact budget is zero", () => { diff --git a/src/skills/loading/skill-contract.ts b/src/skills/loading/skill-contract.ts index 77a4e9a4cafb..0451f04151f9 100644 --- a/src/skills/loading/skill-contract.ts +++ b/src/skills/loading/skill-contract.ts @@ -13,8 +13,6 @@ export interface Skill { readContent?: string; filePath: string; baseDir: string; - /** Deterministic marker for the SKILL.md content rendered as . */ - promptVersion?: string; sourceInfo: SourceInfo; disableModelInvocation: boolean; // Preserve legacy source reads while keeping the canonical upstream shape. @@ -75,7 +73,6 @@ export function formatSkillsForPromptCore(skills: Skill[]): string { const lines = [ "\n\nThe following skills provide specialized instructions for specific tasks.", "Use the read tool to load a skill's file when the task matches its description.", - "If a skill's differs from a previous turn, re-read its SKILL.md before using it.", "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.", "", "", @@ -88,9 +85,6 @@ export function formatSkillsForPromptCore(skills: Skill[]): string { if (skill.locationNote) { lines.push(` ${escapeSkillXml(skill.locationNote)}`); } - if (skill.promptVersion) { - lines.push(` ${escapeSkillXml(skill.promptVersion)}`); - } lines.push(" "); } lines.push(""); @@ -114,7 +108,6 @@ export function formatSkillsCompactForPrompt( descriptionMaxChars > 0 ? "Use the read tool to load a skill's file when the task matches its name or description." : "Use the read tool to load a skill's file when the task matches its name.", - "If a skill's differs from a previous turn, re-read its SKILL.md before using it.", "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.", "", "", @@ -132,9 +125,6 @@ export function formatSkillsCompactForPrompt( if (skill.locationNote) { lines.push(` ${escapeSkillXml(skill.locationNote)}`); } - if (skill.promptVersion) { - lines.push(` ${escapeSkillXml(skill.promptVersion)}`); - } lines.push(" "); } lines.push(""); diff --git a/src/skills/loading/skill-version.ts b/src/skills/loading/skill-version.ts deleted file mode 100644 index 83123cc32205..000000000000 --- a/src/skills/loading/skill-version.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Skill prompt versions are deterministic content markers for model-visible skill catalogs. -import { sha256HexPrefixCore } from "../../infra/crypto-digest.js"; - -export function computeSkillPromptVersion(content: string): string { - return `sha256:${sha256HexPrefixCore(content, 16)}`; -} diff --git a/src/skills/loading/workspace-skill-prompt.test.ts b/src/skills/loading/workspace-skill-prompt.test.ts index 214475275093..8a4f340ff652 100644 --- a/src/skills/loading/workspace-skill-prompt.test.ts +++ b/src/skills/loading/workspace-skill-prompt.test.ts @@ -254,21 +254,17 @@ describe("applySkillsPromptLimits (via buildWorkspaceSkillsPrompt)", () => { expect(prompt.length).toBeLessThanOrEqual(expected.length); }); - it("budgets the final rendered prompt including versions and limit notices", () => { - const skills = Array.from({ length: 24 }, (_, i) => ({ - ...makeSkill(`skill-${i}`, "A".repeat(160)), - promptVersion: `sha256:${String(i).padStart(16, "0")}`, - })); + it("budgets the final rendered prompt including limit notices", () => { + const skills = Array.from({ length: 24 }, (_, i) => makeSkill(`skill-${i}`, "A".repeat(160))); const budget = 2_200; const prompt = buildPrompt(skills, { maxChars: budget }); expect(prompt.length).toBeLessThanOrEqual(budget); - expect(prompt).toContain("sha256:"); expect(prompt).toContain("included"); }); - it("keeps no-skill catalogs empty instead of emitting version guidance", () => { + it("keeps no-skill catalogs empty", () => { const prompt = buildWorkspaceSkillsPrompt("/fake", { entries: [], }); diff --git a/src/skills/loading/workspace-skill-snapshot.test.ts b/src/skills/loading/workspace-skill-snapshot.test.ts index 34cdb5d95704..e5013ac3c309 100644 --- a/src/skills/loading/workspace-skill-snapshot.test.ts +++ b/src/skills/loading/workspace-skill-snapshot.test.ts @@ -428,33 +428,6 @@ describe("buildSkillSnapshot", () => { expect(snapshot.prompt).toBe(prompt); }); - it("renders a deterministic version that changes when SKILL.md content changes", async () => { - const workspaceDir = await fixtureSuite.createCaseDir("workspace"); - const skillDir = path.join(workspaceDir, "skills", "visible"); - await writeSkill({ - dir: skillDir, - name: "visible", - description: "Visible", - body: "# Visible\nfirst body\n", - }); - - const before = buildSnapshot(workspaceDir); - await writeSkill({ - dir: skillDir, - name: "visible", - description: "Visible", - body: "# Visible\nsecond body\n", - }); - const after = buildSnapshot(workspaceDir); - - const beforeVersion = before.prompt.match(/([^<]+)<\/version>/)?.[1]; - const afterVersion = after.prompt.match(/([^<]+)<\/version>/)?.[1]; - expect(beforeVersion).toMatch(/^sha256:[a-f0-9]{16}$/); - expect(afterVersion).toMatch(/^sha256:[a-f0-9]{16}$/); - expect(afterVersion).not.toBe(beforeVersion); - expect(after.prompt).toContain("If a skill's differs from a previous turn"); - }); - it("truncates the skills prompt when it exceeds the configured char budget", async () => { const workspaceDir = await cloneTemplateDir(truncationWorkspaceTemplateDir, "workspace"); @@ -464,7 +437,7 @@ describe("buildSkillSnapshot", () => { skills: { limits: { maxSkillsInPrompt: 100, - maxSkillsPromptChars: 500, + maxSkillsPromptChars: 700, }, }, }, diff --git a/src/skills/runtime/remote-skills.ts b/src/skills/runtime/remote-skills.ts index 8369213c457b..a4fb8b0628e9 100644 --- a/src/skills/runtime/remote-skills.ts +++ b/src/skills/runtime/remote-skills.ts @@ -3,7 +3,6 @@ import { createSyntheticSourceInfo } from "../../agents/sessions/source-info.js" import { createSubsystemLogger } from "../../logging/subsystem.js"; import { resolveNodeIdFromNodeList } from "../../shared/node-resolve.js"; import { parseSkillFrontmatter, resolveSkillInvocationPolicy } from "../loading/frontmatter.js"; -import { computeSkillPromptVersion } from "../loading/skill-version.js"; import type { ParsedSkillFrontmatter, SkillEntry } from "../types.js"; import { bumpSkillsSnapshotVersion } from "./refresh-state.js"; @@ -236,7 +235,6 @@ export function mergeRemoteNodeSkillEntries( readContent: skill.content, filePath, baseDir: filePath.slice(0, -"/SKILL.md".length), - promptVersion: computeSkillPromptVersion(skill.content), source: "openclaw-node", sourceInfo: createSyntheticSourceInfo(filePath, { source: "openclaw-node", diff --git a/src/skills/test-support/test-helpers.ts b/src/skills/test-support/test-helpers.ts index 9a996dbd1b22..53289650488b 100644 --- a/src/skills/test-support/test-helpers.ts +++ b/src/skills/test-support/test-helpers.ts @@ -8,7 +8,6 @@ export function createCanonicalFixtureSkill(params: { filePath: string; baseDir: string; source: string; - promptVersion?: string; disableModelInvocation?: boolean; }): Skill { return { @@ -16,7 +15,6 @@ export function createCanonicalFixtureSkill(params: { description: params.description, filePath: params.filePath, baseDir: params.baseDir, - promptVersion: params.promptVersion, source: params.source, sourceInfo: createSyntheticSourceInfo(params.filePath, { source: params.source, diff --git a/src/skills/types.ts b/src/skills/types.ts index 747ade899f92..efde38ccc0c7 100644 --- a/src/skills/types.ts +++ b/src/skills/types.ts @@ -124,7 +124,7 @@ export type SkillEligibilityContext = { }; }; -export const WORKSPACE_SKILLS_PROMPT_FORMAT_VERSION = 3; +export const WORKSPACE_SKILLS_PROMPT_FORMAT_VERSION = 4; export type SkillSnapshot = { prompt: string;