fix(observabilidade): stream cleanup, outcome logic e NaN guard no parser
This commit is contained in:
@@ -46,11 +46,11 @@ function classifyEventType(raw: Record<string, unknown>): SessionEventType {
|
||||
function deriveOutcome(events: SessionEvent[]): SessionOutcome {
|
||||
if (events.length === 0) return 'unknown'
|
||||
const last = events[events.length - 1]
|
||||
if (last.type !== 'assistant') return 'interrupted'
|
||||
const raw = last.raw as { message?: { stop_reason?: string } }
|
||||
if (last.type === 'assistant' && raw.message?.stop_reason && raw.message.stop_reason !== 'end_turn' && raw.message.stop_reason !== 'tool_use') {
|
||||
if (raw.message.stop_reason === 'error') return 'error'
|
||||
}
|
||||
if (last.type === 'assistant') return 'completed'
|
||||
const stopReason = raw.message?.stop_reason
|
||||
if (stopReason === 'error') return 'error'
|
||||
if (stopReason === 'end_turn' || stopReason === 'tool_use' || !stopReason) return 'completed'
|
||||
return 'interrupted'
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ export async function parseSessionFile(jsonlPath: string): Promise<ParseResult>
|
||||
let lastTs: string | null = null
|
||||
let idx = 0
|
||||
|
||||
try {
|
||||
for await (const line of rl) {
|
||||
if (!line.trim()) continue
|
||||
let raw: Record<string, unknown>
|
||||
@@ -148,12 +149,20 @@ export async function parseSessionFile(jsonlPath: string): Promise<ParseResult>
|
||||
hook_name: hook,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
rl.close()
|
||||
stream.destroy()
|
||||
}
|
||||
|
||||
if (!sessionId) {
|
||||
sessionId = basename(jsonlPath, '.jsonl')
|
||||
}
|
||||
|
||||
const durationSec = firstTs && lastTs ? Math.max(0, Math.round((Date.parse(lastTs) - Date.parse(firstTs)) / 1000)) : null
|
||||
let durationSec: number | null = null
|
||||
if (firstTs && lastTs) {
|
||||
const diff = Date.parse(lastTs) - Date.parse(firstTs)
|
||||
if (Number.isFinite(diff)) durationSec = Math.max(0, Math.round(diff / 1000))
|
||||
}
|
||||
const outcome = deriveOutcome(events)
|
||||
const stats = statSync(jsonlPath)
|
||||
|
||||
|
||||
@@ -76,4 +76,25 @@ describe('parseSessionFile', () => {
|
||||
const result = await parseSessionFile(path)
|
||||
expect(result.meta.user_messages).toBe(1)
|
||||
})
|
||||
|
||||
it('devolve duration_sec null quando timestamps são inválidos', async () => {
|
||||
const path = writeJsonl([
|
||||
{ type: 'user', timestamp: 'not-a-date', message: { role: 'user', content: [{ type: 'text', text: 'a' }] } },
|
||||
{ type: 'assistant', timestamp: 'also-not-a-date', message: { role: 'assistant', content: [{ type: 'text', text: 'b' }] } },
|
||||
])
|
||||
const result = await parseSessionFile(path)
|
||||
expect(result.meta.duration_sec).toBeNull()
|
||||
})
|
||||
|
||||
it('classifica max_tokens como interrupted', async () => {
|
||||
const path = writeJsonl([
|
||||
{
|
||||
type: 'assistant',
|
||||
timestamp: '2026-04-23T10:00:00Z',
|
||||
message: { role: 'assistant', content: [{ type: 'text', text: 'resposta cortada' }], stop_reason: 'max_tokens' },
|
||||
},
|
||||
])
|
||||
const result = await parseSessionFile(path)
|
||||
expect(result.meta.outcome).toBe('interrupted')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user