mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-04 13:51:50 +00:00
fix(classifier): attribute skills to skillBreakdown regardless of turn category
Skill attribution was gated on the turn classifying as 'general', but classifyByToolPattern checks hasSkill last, so any turn invoking the Skill tool alongside Read/Bash/Edit/MCP got a real category and its skill was silently dropped. Skill turns virtually always carry other tools, which is why skills came back empty in all 25 usage snapshots (#741) while mcpServers populated. subCategory is now set for any turn with Skill tool_use; category assignment is untouched. The claude parse version is bumped so cached session summaries (which lack skill attribution) re-parse on upgrade.
This commit is contained in:
parent
1850276bd1
commit
56e836fa9e
4 changed files with 38 additions and 7 deletions
|
|
@ -208,10 +208,8 @@ export function classifyTurn(turn: ParsedTurn): ClassifiedTurn {
|
|||
|
||||
const result: ClassifiedTurn = { ...turn, category, retries: countRetries(turn), hasEdits: turnHasEdits(turn) }
|
||||
|
||||
if (category === 'general') {
|
||||
const skills = getAllSkills(turn)
|
||||
if (skills.length > 0) result.subCategory = skills[0]
|
||||
}
|
||||
const skills = getAllSkills(turn)
|
||||
if (skills.length > 0) result.subCategory = skills[0]
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ export const DURABLE_PROVIDER_NAMES: ReadonlySet<string> = new Set(['copilot'])
|
|||
// re-parse, which lands the flag too, and durable orphans now survive
|
||||
// fingerprint changes (the carry-forward in getOrCreateProviderSection).
|
||||
export const PROVIDER_PARSE_VERSIONS: Record<string, string> = {
|
||||
claude: 'advisor-usage-v1',
|
||||
claude: 'advisor-usage-v1-skills',
|
||||
cline: 'worktree-project-grouping-v1',
|
||||
codewhale: 'aggregate-session-v1-est-cost',
|
||||
// Bump when the Codex parser changes attribution so unchanged, already-cached
|
||||
|
|
|
|||
|
|
@ -79,11 +79,11 @@ describe('classifyTurn — Skill subCategory', () => {
|
|||
expect(c.subCategory).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does not attach subCategory when category is not general (e.g. Skill alongside Edit promotes to coding)', () => {
|
||||
it('attaches subCategory without changing the category when Skill fires alongside Edit', () => {
|
||||
const turn = makeTurn([makeCall({ tools: ['Skill', 'Edit'], skills: ['init'] })])
|
||||
const c = classifyTurn(turn)
|
||||
expect(c.category).toBe('coding')
|
||||
expect(c.subCategory).toBeUndefined()
|
||||
expect(c.subCategory).toBe('init')
|
||||
})
|
||||
|
||||
it('does not attach subCategory for non-Skill general turns', () => {
|
||||
|
|
|
|||
|
|
@ -455,3 +455,36 @@ describe('(f) durable orphans survive a parse-version bump', () => {
|
|||
expect(again).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// (g) Skill attribution is independent of turn category
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
describe('(g) skill attribution is independent of turn category', () => {
|
||||
it('puts a Skill + Edit turn in skillBreakdown while preserving coding category', async () => {
|
||||
const synthFile = join(tmpHome, 'synth-skill.txt')
|
||||
await writeFile(synthFile, 'placeholder')
|
||||
|
||||
_synthSources = [{ path: synthFile, project: 'test', provider: 'test-synthetic' }]
|
||||
_synthYields = [{
|
||||
provider: 'test-synthetic', model: 'gpt-4o',
|
||||
inputTokens: 10, outputTokens: 5,
|
||||
cacheCreationInputTokens: 0, cacheReadInputTokens: 0,
|
||||
cachedInputTokens: 0, reasoningTokens: 0, webSearchRequests: 0,
|
||||
costUSD: 0.001, tools: ['Skill', 'Edit'], bashCommands: [],
|
||||
skills: ['telemetry-review'],
|
||||
timestamp: '2026-07-18T12:00:00.000Z',
|
||||
speed: 'standard',
|
||||
deduplicationKey: 'synth-skill-edit',
|
||||
userMessage: '', sessionId: 'synth-skill-session',
|
||||
}]
|
||||
|
||||
const projects = await parseAllSessions(undefined, 'test-synthetic')
|
||||
const session = projects.flatMap(project => project.sessions)[0]
|
||||
|
||||
expect(session).toBeDefined()
|
||||
expect(session!.turns[0]!.category).toBe('coding')
|
||||
expect(session!.turns[0]!.subCategory).toBe('telemetry-review')
|
||||
expect(session!.categoryBreakdown.coding.turns).toBe(1)
|
||||
expect(session!.skillBreakdown['telemetry-review']?.turns).toBe(1)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue