From a87bbc69835c9468557b95e9507f8ddb1580db6e Mon Sep 17 00:00:00 2001 From: William Valentin Date: Fri, 20 Mar 2026 11:17:40 -0700 Subject: [PATCH] fix(claude-hook): derive span durations from start timestamps --- hooks/claude-code/handler.js | 30 +++++++++++++++++++++++++++--- hooks/claude-code/handler.ts | 28 +++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/hooks/claude-code/handler.js b/hooks/claude-code/handler.js index 53ad357..10ce6ee 100755 --- a/hooks/claude-code/handler.js +++ b/hooks/claude-code/handler.js @@ -331,6 +331,8 @@ async function handleToolStart(input) { if (spanKey) { activeSpans.set(spanKey, spanId); state.spans[spanKey] = spanId; + state.spanStartTimes = state.spanStartTimes || {}; + state.spanStartTimes[spanId] = Date.now(); if (runId) state.runId = runId; if (sessionKey) @@ -358,7 +360,13 @@ async function handleToolEnd(input) { const spanId = spanKey ? activeSpans.get(spanKey) || state.spans[spanKey] : void 0; const result = isRecord(input.result) ? input.result : isRecord(input.output) ? input.output : {}; const success = !result.error; - const duration = pickNumber(input.duration_ms, input.elapsed_ms); + const startTime = spanId ? state.spanStartTimes?.[spanId] : void 0; + const duration = pickNumber(input.duration_ms, input.elapsed_ms) ?? (startTime ? Date.now() - startTime : void 0); + if (spanId && state.spanStartTimes) { + delete state.spanStartTimes[spanId]; + if (sessionKey) + saveState(sessionKey, state); + } enqueue(buildEnvelope("span.end", sessionKey, { runId, spanId, @@ -387,6 +395,8 @@ async function handleSubagentStart(input) { if (sessionKey) { activeSubagents.set(sessionKey, { name: agentName, spanId }); state.subagent = { name: agentName, spanId }; + state.spanStartTimes = state.spanStartTimes || {}; + state.spanStartTimes[spanId] = Date.now(); if (runId) state.runId = runId; saveState(sessionKey, state); @@ -412,8 +422,14 @@ async function handleSubagentStop(input) { const subagent = sessionKey ? activeSubagents.get(sessionKey) || state.subagent : void 0; const spanId = subagent?.spanId; const agentName = subagent?.name || pickString(input.agent, input.agent_name) || "unknown"; - const duration = pickNumber(input.duration_ms, input.elapsed_ms); + const startTime = spanId ? state.spanStartTimes?.[spanId] : void 0; + const duration = pickNumber(input.duration_ms, input.elapsed_ms) ?? (startTime ? Date.now() - startTime : void 0); const usage = getUsage(input); + if (spanId && state.spanStartTimes) { + delete state.spanStartTimes[spanId]; + if (sessionKey) + saveState(sessionKey, state); + } enqueue(buildEnvelope("span.end", sessionKey, { runId, spanId, @@ -439,6 +455,8 @@ async function handleCompactStart(input) { if (sessionKey) { activeSpans.set(sessionKey + ":compact", spanId); state.compactSpanId = spanId; + state.spanStartTimes = state.spanStartTimes || {}; + state.spanStartTimes[spanId] = Date.now(); if (runId) state.runId = runId; saveState(sessionKey, state); @@ -459,8 +477,14 @@ async function handleCompactEnd(input) { const runId = sessionKey ? activeRuns.get(sessionKey) || state.runId : void 0; const spanKey = sessionKey ? sessionKey + ":compact" : void 0; const spanId = spanKey ? activeSpans.get(spanKey) || state.compactSpanId : void 0; - const duration = pickNumber(input.duration_ms, input.elapsed_ms); + const startTime = spanId ? state.spanStartTimes?.[spanId] : void 0; + const duration = pickNumber(input.duration_ms, input.elapsed_ms) ?? (startTime ? Date.now() - startTime : void 0); const contextWindow = getContextWindow(input); + if (spanId && state.spanStartTimes) { + delete state.spanStartTimes[spanId]; + if (sessionKey) + saveState(sessionKey, state); + } enqueue(buildEnvelope("span.end", sessionKey, { runId, spanId, diff --git a/hooks/claude-code/handler.ts b/hooks/claude-code/handler.ts index 5ae91ce..4343e76 100644 --- a/hooks/claude-code/handler.ts +++ b/hooks/claude-code/handler.ts @@ -17,6 +17,7 @@ interface Dict { [key: string]: any } interface SessionState { runId?: string; spans: { [key: string]: string }; // key = sessionKey:toolName, value = spanId + spanStartTimes?: { [spanId: string]: number }; // spanId -> epoch ms subagent?: { name: string; spanId: string }; compactSpanId?: string; } @@ -385,6 +386,8 @@ async function handleToolStart(input: Dict) { if (spanKey) { activeSpans.set(spanKey, spanId); state.spans[spanKey] = spanId; + state.spanStartTimes = state.spanStartTimes || {}; + state.spanStartTimes[spanId] = Date.now(); if (runId) state.runId = runId; if (sessionKey) saveState(sessionKey, state); } @@ -413,7 +416,12 @@ async function handleToolEnd(input: Dict) { const spanId = spanKey ? (activeSpans.get(spanKey) || state.spans[spanKey]) : undefined; const result = isRecord(input.result) ? input.result : isRecord(input.output) ? input.output : {}; const success = !result.error; - const duration = pickNumber(input.duration_ms, input.elapsed_ms); + const startTime = spanId ? state.spanStartTimes?.[spanId] : undefined; + const duration = pickNumber(input.duration_ms, input.elapsed_ms) ?? (startTime ? Date.now() - startTime : undefined); + if (spanId && state.spanStartTimes) { + delete state.spanStartTimes[spanId]; + if (sessionKey) saveState(sessionKey, state); + } enqueue(buildEnvelope('span.end', sessionKey, { runId, @@ -448,6 +456,8 @@ async function handleSubagentStart(input: Dict) { if (sessionKey) { activeSubagents.set(sessionKey, { name: agentName, spanId }); state.subagent = { name: agentName, spanId }; + state.spanStartTimes = state.spanStartTimes || {}; + state.spanStartTimes[spanId] = Date.now(); if (runId) state.runId = runId; saveState(sessionKey, state); } @@ -475,8 +485,13 @@ async function handleSubagentStop(input: Dict) { const subagent = sessionKey ? (activeSubagents.get(sessionKey) || state.subagent) : undefined; const spanId = subagent?.spanId; const agentName = subagent?.name || pickString(input.agent, input.agent_name) || 'unknown'; - const duration = pickNumber(input.duration_ms, input.elapsed_ms); + const startTime = spanId ? state.spanStartTimes?.[spanId] : undefined; + const duration = pickNumber(input.duration_ms, input.elapsed_ms) ?? (startTime ? Date.now() - startTime : undefined); const usage = getUsage(input); + if (spanId && state.spanStartTimes) { + delete state.spanStartTimes[spanId]; + if (sessionKey) saveState(sessionKey, state); + } enqueue(buildEnvelope('span.end', sessionKey, { runId, @@ -507,6 +522,8 @@ async function handleCompactStart(input: Dict) { if (sessionKey) { activeSpans.set(sessionKey + ':compact', spanId); state.compactSpanId = spanId; + state.spanStartTimes = state.spanStartTimes || {}; + state.spanStartTimes[spanId] = Date.now(); if (runId) state.runId = runId; saveState(sessionKey, state); } @@ -529,8 +546,13 @@ async function handleCompactEnd(input: Dict) { const runId = sessionKey ? (activeRuns.get(sessionKey) || state.runId) : undefined; const spanKey = sessionKey ? sessionKey + ':compact' : undefined; const spanId = spanKey ? (activeSpans.get(spanKey) || state.compactSpanId) : undefined; - const duration = pickNumber(input.duration_ms, input.elapsed_ms); + const startTime = spanId ? state.spanStartTimes?.[spanId] : undefined; + const duration = pickNumber(input.duration_ms, input.elapsed_ms) ?? (startTime ? Date.now() - startTime : undefined); const contextWindow = getContextWindow(input); + if (spanId && state.spanStartTimes) { + delete state.spanStartTimes[spanId]; + if (sessionKey) saveState(sessionKey, state); + } enqueue(buildEnvelope('span.end', sessionKey, { runId,