From e9ae836a46336e8738df3dedd48ea46c9f071806 Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 24 Jul 2026 09:21:58 +0000 Subject: [PATCH] fix(coding-agent): count only rendered skills in /context accounting computeNonMessageBreakdown estimated Skills tokens from the unfiltered session.skills registry and subtracted that from the first system-prompt block, which only ever contained the rendered (filtered) skills. Hidden explicit-only skills (hide/disable-model-invocation) and all skills when the read tool was absent inflated Skills and clamped System prompt to 0. Add a renderedSkills helper mirroring buildSystemPrompt's filter and use it for the Skills estimate so the category split matches the provider prompt. Fixes #6498 --- packages/coding-agent/CHANGELOG.md | 4 +++ .../src/modes/utils/context-usage.ts | 21 ++++++++++-- .../test/modes/context-usage.test.ts | 33 +++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f588ade0e..52dc33112 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `/context` counting hidden, explicit-only skills (`hide: true` / `disable-model-invocation`) in the Skills category and subtracting that inflated estimate from the first system-prompt block, which reported `System prompt: 0 tokens` and inflated Skills usage. Accounting now counts only the skills actually rendered into the system prompt — mirroring `buildSystemPrompt`'s filter, so hidden skills and all skills when the `read` tool is unavailable contribute zero ([#6498](https://github.com/can1357/oh-my-pi/issues/6498)). + ## [17.1.1] - 2026-07-24 ### Added diff --git a/packages/coding-agent/src/modes/utils/context-usage.ts b/packages/coding-agent/src/modes/utils/context-usage.ts index 2ea092585..c26e64cf1 100644 --- a/packages/coding-agent/src/modes/utils/context-usage.ts +++ b/packages/coding-agent/src/modes/utils/context-usage.ts @@ -56,6 +56,22 @@ const EMPTY_STRING_PARTS: string[] = []; const EMPTY_TOOLS: ReadonlyArray> = []; const EMPTY_SKILLS: readonly Skill[] = []; +/** + * Skills actually rendered into the system prompt, mirroring the filter in + * `buildSystemPrompt` (`system-prompt.ts`): the `read` tool must be present so + * the model can fetch skill content, and skills with frontmatter `hide: true` + * (or `disable-model-invocation`, normalized onto `hide`) are excluded. + * Accounting must count only these so the Skills category and the System-prompt + * subtraction stay aligned with the provider-facing prompt. + */ +function renderedSkills( + skills: readonly Skill[], + tools: ReadonlyArray>, +): readonly Skill[] { + if (!tools.some(tool => tool.name === "read")) return EMPTY_SKILLS; + return skills.filter(skill => skill.hide !== true); +} + export function estimateSkillsTokens(skills: readonly Skill[]): number { const fragments: string[] = []; for (const skill of skills) { @@ -171,8 +187,9 @@ export function computeNonMessageBreakdown(session: NonMessageTokenSource): { } { const entry = nonMessageTokenCacheEntry(session); if (entry.breakdown) return entry.breakdown; - const skillsTokens = estimateSkillsTokens(session.skills ?? EMPTY_SKILLS); - const toolsTokens = estimateToolSchemaTokens(session.agent?.state?.tools ?? EMPTY_TOOLS); + const tools = session.agent?.state?.tools ?? EMPTY_TOOLS; + const skillsTokens = estimateSkillsTokens(renderedSkills(session.skills ?? EMPTY_SKILLS, tools)); + const toolsTokens = estimateToolSchemaTokens(tools); const systemPromptParts = session.systemPrompt ?? EMPTY_STRING_PARTS; const systemContextTokens = countTokens(systemPromptParts.slice(1)); const systemPromptTokens = Math.max(0, countTokens(systemPromptParts[0] ?? "") - skillsTokens); diff --git a/packages/coding-agent/test/modes/context-usage.test.ts b/packages/coding-agent/test/modes/context-usage.test.ts index 73408332f..16807e69b 100644 --- a/packages/coding-agent/test/modes/context-usage.test.ts +++ b/packages/coding-agent/test/modes/context-usage.test.ts @@ -137,3 +137,36 @@ describe("computeNonMessageTokens / computeNonMessageBreakdown memoization", () expect(computeNonMessageBreakdown(session as never).systemPromptTokens).not.toBe(breakdown.systemPromptTokens); }); }); + +/** + * Contract: the Skills category counts only skills actually rendered into the + * system prompt (mirroring `buildSystemPrompt`'s filter) — hidden/explicit-only + * skills, and every skill when the `read` tool is absent, contribute zero. The + * System-prompt subtraction must not be inflated by unrendered skill metadata + * and clamped to 0 (issue #6498). + */ +describe("computeNonMessageBreakdown skills filtering", () => { + const readTool = { name: "read", description: "read files", parameters: {} }; + const hidden = { name: "hidden-skill", description: "X".repeat(4000), filePath: "/s/h.md", hide: true }; + const visible = { name: "vis", description: "small visible skill", filePath: "/s/v.md" }; + // First prompt block as rendered: only the visible skill appears. + const renderedPrompt = "You are an agent.\nSkills:\n- vis: small visible skill\n"; + + function session(tools: unknown[], skills: unknown[]) { + return { systemPrompt: [renderedPrompt], agent: { state: { tools } }, skills } as never; + } + + it("excludes hidden skills and does not clamp System prompt to 0", () => { + const b = computeNonMessageBreakdown(session([readTool], [hidden, visible])); + // Only the visible skill is counted, not the large hidden one. + expect(b.skillsTokens).toBe(computeNonMessageBreakdown(session([readTool], [visible])).skillsTokens); + expect(b.skillsTokens).toBeLessThan(100); + expect(b.systemPromptTokens).toBeGreaterThan(0); + }); + + it("counts zero Skills tokens when the read tool is unavailable", () => { + const b = computeNonMessageBreakdown(session([], [hidden, visible])); + expect(b.skillsTokens).toBe(0); + expect(b.systemPromptTokens).toBe(computeNonMessageBreakdown(session([], [])).systemPromptTokens); + }); +});