From ac4e9c6f357439bf3d77b5ab0f82f3eafc52821e Mon Sep 17 00:00:00 2001 From: Emanuel Almeida Date: Thu, 23 Apr 2026 02:20:24 +0100 Subject: [PATCH] fix(observabilidade): parser extrai skills de tool_result.content (string e array) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Antes: skills_invoked vazio em 1608/1608 sessões porque detectSkillInvoked apenas era aplicado ao text extraído de content[type=text]. A string 'Launching skill: X' vive dentro de tool_result.content (string ou array de text blocks), que era ignorada. Fix: adicionar helper extractResultText(r) que trata ambos os casos e aplicar detectSkillInvoked + detectHook também ao tool_result. Após re-indexação full, 526/1616 sessões têm agora skills detectadas e o detector de padrões devolve 6 padrões (vs 2 baseline), incluindo skills_with_high_error_rate reais. Co-Authored-By: Claude Opus 4.7 (1M context) --- api/services/sessions/parser.ts | 24 ++++++++++++++++--- api/tests/sessions-parser.test.ts | 38 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/api/services/sessions/parser.ts b/api/services/sessions/parser.ts index 5003383..f079255 100644 --- a/api/services/sessions/parser.ts +++ b/api/services/sessions/parser.ts @@ -19,6 +19,21 @@ function detectHook(text: string | null): string | null { return m ? m[1] : null } +function extractResultText(r: unknown): string | null { + if (r == null) return null + if (typeof r === 'string') return r + if (Array.isArray(r)) { + const parts: string[] = [] + for (const p of r) { + if (p && typeof p === 'object' && 'text' in p && typeof (p as { text: unknown }).text === 'string') { + parts.push((p as { text: string }).text) + } + } + return parts.length ? parts.join('\n') : null + } + return null +} + function extractText(rawMsg: unknown): string | null { if (!rawMsg || typeof rawMsg !== 'object') return null const msg = rawMsg as { content?: unknown } @@ -132,9 +147,12 @@ export async function parseSessionFile(jsonlPath: string): Promise } } + const resultText = extractResultText(toolResult) const skill = detectSkillInvoked(text) - if (skill) skillsInvoked.add(skill) - const hook = detectHook(text) + const skillFromResult = detectSkillInvoked(resultText) + const finalSkill = skill ?? skillFromResult + if (finalSkill) skillsInvoked.add(finalSkill) + const hook = detectHook(text) ?? detectHook(resultText) events.push({ index: idx++, @@ -145,7 +163,7 @@ export async function parseSessionFile(jsonlPath: string): Promise tool_name: toolName, tool_input: toolInput, tool_result: toolResult, - skill_invoked: skill, + skill_invoked: finalSkill, hook_name: hook, }) } diff --git a/api/tests/sessions-parser.test.ts b/api/tests/sessions-parser.test.ts index a456a4b..6669c57 100644 --- a/api/tests/sessions-parser.test.ts +++ b/api/tests/sessions-parser.test.ts @@ -67,6 +67,44 @@ describe('parseSessionFile', () => { expect(result.meta.skills_invoked).toContain('superpowers:brainstorming') }) + it('detecta skill invocation em tool_result.content (string)', async () => { + const path = writeJsonl([ + { + type: 'user', + timestamp: '2026-04-23T10:00:00Z', + message: { + role: 'user', + content: [ + { type: 'tool_result', tool_use_id: 'abc', content: 'Launching skill: infraestrutura:easypanel-monitor\nOther log output' }, + ], + }, + }, + ]) + const result = await parseSessionFile(path) + expect(result.meta.skills_invoked).toContain('infraestrutura:easypanel-monitor') + }) + + it('detecta skill invocation em tool_result.content (array de text blocks)', async () => { + const path = writeJsonl([ + { + type: 'user', + timestamp: '2026-04-23T10:00:00Z', + message: { + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'abc', + content: [{ type: 'text', text: 'Launching skill: superpowers:brainstorming' }], + }, + ], + }, + }, + ]) + const result = await parseSessionFile(path) + expect(result.meta.skills_invoked).toContain('superpowers:brainstorming') + }) + it('ignora linhas JSON inválidas silenciosamente', async () => { const path = writeJsonl([ { type: 'user', message: { role: 'user', content: [{ type: 'text', text: 'válido' }] } },