Merge pull request #911 from ozymandiashh/fix/swarm-context-budget

fix(context-budget): stop double-counting home skills and CLAUDE.md
This commit is contained in:
Resham Joshi 2026-08-04 03:13:29 -07:00 committed by GitHub
commit 9372fb56b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 58 additions and 2 deletions

View file

@ -60,8 +60,13 @@ async function countMcpTools(projectPath?: string): Promise<number> {
}
async function countSkills(projectPath?: string): Promise<number> {
const dirs = [join(homedir(), '.claude', 'skills')]
if (projectPath) dirs.push(join(projectPath, '.claude', 'skills'))
// Dedupe by resolved path: when the project IS the home dir, the home and
// project skills dirs are the same directory, and counting both double-counts
// every skill (and inflates the context budget).
const dirs = [...new Set([
join(homedir(), '.claude', 'skills'),
...(projectPath ? [join(projectPath, '.claude', 'skills')] : []),
])]
let count = 0
for (const dir of dirs) {
@ -91,7 +96,12 @@ async function scanMemoryFiles(projectPath?: string): Promise<Array<{ name: stri
paths.push({ path: join(projectPath, 'CLAUDE.local.md'), name: 'CLAUDE.local.md' })
}
// Dedupe by path so a project that IS the home dir does not read (and count)
// ~/.claude/CLAUDE.md twice.
const seenPaths = new Set<string>()
for (const { path, name } of paths) {
if (seenPaths.has(path)) continue
seenPaths.add(path)
if (!existsSync(path)) continue
const content = await readSessionFile(path)
if (content === null) continue

View file

@ -0,0 +1,46 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
import { join } from 'path'
// Mock homedir to a temp dir so "project == home" is reproducible.
import { vi } from 'vitest'
vi.mock('os', async () => {
const actual = await vi.importActual<typeof import('os')>('os')
const fs = await vi.importActual<typeof import('fs')>('fs')
const fakeHome = fs.mkdtempSync(actual.tmpdir() + '/cb-ctxbudget-home-')
process.env['CB_CTXBUDGET_FAKE_HOME'] = fakeHome
return { ...actual, homedir: () => fakeHome }
})
const HOME = process.env['CB_CTXBUDGET_FAKE_HOME']!
import { estimateContextBudget } from '../src/context-budget.js'
describe('context budget: no double-count when the project IS the home dir', () => {
beforeEach(() => {
rmSync(join(HOME, '.claude'), { recursive: true, force: true })
mkdirSync(join(HOME, '.claude', 'skills', 'my-skill'), { recursive: true })
writeFileSync(join(HOME, '.claude', 'skills', 'my-skill', 'SKILL.md'), '# Skill')
writeFileSync(join(HOME, '.claude', 'CLAUDE.md'), 'home memory')
})
it('counts the one home skill once, not twice, when projectPath is home', async () => {
// With projectPath === home, the home and project skills dirs resolve to
// the same directory; the unfixed code pushed both and counted every skill
// twice (and read ~/.claude/CLAUDE.md twice).
const budget = await estimateContextBudget(HOME)
expect(budget.skills.count).toBe(1)
// ~/.claude/CLAUDE.md must appear once in the memory file list.
const homeMemory = budget.memory.files.filter(f => f.name.includes('.claude/CLAUDE.md'))
expect(homeMemory).toHaveLength(1)
})
it('still counts a distinct project skill separately from a home skill', async () => {
const proj = mkdtempSync(join(HOME, '..', 'cb-ctxbudget-proj-'))
mkdirSync(join(proj, '.claude', 'skills', 'proj-skill'), { recursive: true })
writeFileSync(join(proj, '.claude', 'skills', 'proj-skill', 'SKILL.md'), '# Proj')
const budget = await estimateContextBudget(proj)
expect(budget.skills.count).toBe(2) // home skill + project skill
rmSync(proj, { recursive: true, force: true })
})
})