feat(hooks): emit per-run token usage and duration on run.end
The stats layer reads usage/duration only from run.end, but neither framework populated them, so tokens/cost/avg-duration were always 0. - hermes: accumulate token usage across each run's api-result calls in session state and attach the summed usage plus a computed duration_ms (from a stored runStartedAt) onto run.end. metric.snapshot emission is unchanged, so there is no double counting. - claude-code: store runStartedAt and use it as a duration_ms fallback at all run.end sites. Usage is unavailable from CC hook inputs. Live verification: a real hermes run now reports duration_ms and total_tokens on run.end; dashboard tokens_today/avg_duration_ms, both previously 0, now populate. cost_today stays 0 (no provider emits cost through the hooks). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -335,7 +335,7 @@ async function handleSessionStart(input) {
|
||||
}
|
||||
const runId = randomUUID2();
|
||||
activeRuns.set(sessionKey, runId);
|
||||
saveState(sessionKey, { runId, spans: {} });
|
||||
saveState(sessionKey, { runId, runStartedAt: Date.now(), spans: {} });
|
||||
const contextWindow = getContextWindow(input);
|
||||
enqueue(buildEnvelope(FRAMEWORK, HOST, "session.start", sessionKey, {
|
||||
attributes: {
|
||||
@@ -362,7 +362,7 @@ async function handleSessionEnd(input) {
|
||||
const runId = sessionKey ? activeRuns.get(sessionKey) || state.runId : void 0;
|
||||
const usage = getUsage(input);
|
||||
const contextWindow = getContextWindow(input);
|
||||
const duration = pickNumber(input.duration_ms, input.elapsed_ms);
|
||||
const duration = pickNumber(input.duration_ms, input.elapsed_ms) ?? (state.runStartedAt ? Date.now() - state.runStartedAt : void 0);
|
||||
if (runId) {
|
||||
enqueue(buildEnvelope(FRAMEWORK, HOST, "run.end", sessionKey, {
|
||||
runId,
|
||||
@@ -398,7 +398,7 @@ async function handlePromptSubmit(input) {
|
||||
runId,
|
||||
payload: {
|
||||
status: "success",
|
||||
duration_ms: pickNumber(input.elapsed_ms, input.duration_ms)
|
||||
duration_ms: pickNumber(input.elapsed_ms, input.duration_ms) ?? (state.runStartedAt ? Date.now() - state.runStartedAt : void 0)
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -414,7 +414,7 @@ async function handlePromptSubmit(input) {
|
||||
const newRunId = randomUUID2();
|
||||
if (sessionKey) {
|
||||
activeRuns.set(sessionKey, newRunId);
|
||||
saveState(sessionKey, { runId: newRunId, spans: {} });
|
||||
saveState(sessionKey, { runId: newRunId, runStartedAt: Date.now(), spans: {} });
|
||||
}
|
||||
enqueue(buildEnvelope(FRAMEWORK, HOST, "run.start", sessionKey, {
|
||||
runId: newRunId,
|
||||
@@ -613,10 +613,10 @@ async function handleNotification(input) {
|
||||
const notificationType = pickString(input.notification_type, input.type);
|
||||
const usage = getUsage(input);
|
||||
const contextWindow = getContextWindow(input);
|
||||
const duration = pickNumber(input.duration_ms, input.elapsed_ms);
|
||||
if (notificationType === "Done" || notificationType === "success") {
|
||||
const state = sessionKey ? loadState(sessionKey) : { spans: {} };
|
||||
const runId = sessionKey ? activeRuns.get(sessionKey) || state.runId : void 0;
|
||||
const duration = pickNumber(input.duration_ms, input.elapsed_ms) ?? (state.runStartedAt ? Date.now() - state.runStartedAt : void 0);
|
||||
if (runId) {
|
||||
enqueue(buildEnvelope(FRAMEWORK, HOST, "run.end", sessionKey, {
|
||||
runId,
|
||||
|
||||
@@ -24,6 +24,7 @@ const { enqueue, flush } = createTransport(INGEST_URL);
|
||||
// ── Persisted state (survives between hook subprocess invocations) ──────────
|
||||
interface SessionState {
|
||||
runId?: string;
|
||||
runStartedAt?: number; // epoch ms when the current run began
|
||||
spans: { [key: string]: string }; // key = sessionKey:toolName, value = spanId
|
||||
spanStartTimes?: { [spanId: string]: number }; // spanId -> epoch ms
|
||||
subagent?: { name: string; spanId: string };
|
||||
@@ -198,7 +199,7 @@ async function handleSessionStart(input: Dict) {
|
||||
|
||||
const runId = randomUUID();
|
||||
activeRuns.set(sessionKey, runId);
|
||||
saveState(sessionKey, { runId, spans: {} });
|
||||
saveState(sessionKey, { runId, runStartedAt: Date.now(), spans: {} });
|
||||
|
||||
const contextWindow = getContextWindow(input);
|
||||
|
||||
@@ -230,7 +231,7 @@ async function handleSessionEnd(input: Dict) {
|
||||
const runId = sessionKey ? (activeRuns.get(sessionKey) || state.runId) : undefined;
|
||||
const usage = getUsage(input);
|
||||
const contextWindow = getContextWindow(input);
|
||||
const duration = pickNumber(input.duration_ms, input.elapsed_ms);
|
||||
const duration = pickNumber(input.duration_ms, input.elapsed_ms) ?? (state.runStartedAt ? Date.now() - state.runStartedAt : undefined);
|
||||
|
||||
if (runId) {
|
||||
enqueue(buildEnvelope(FRAMEWORK, HOST, 'run.end', sessionKey, {
|
||||
@@ -270,7 +271,7 @@ async function handlePromptSubmit(input: Dict) {
|
||||
runId,
|
||||
payload: {
|
||||
status: 'success',
|
||||
duration_ms: pickNumber(input.elapsed_ms, input.duration_ms),
|
||||
duration_ms: pickNumber(input.elapsed_ms, input.duration_ms) ?? (state.runStartedAt ? Date.now() - state.runStartedAt : undefined),
|
||||
},
|
||||
}));
|
||||
}
|
||||
@@ -288,7 +289,7 @@ async function handlePromptSubmit(input: Dict) {
|
||||
const newRunId = randomUUID();
|
||||
if (sessionKey) {
|
||||
activeRuns.set(sessionKey, newRunId);
|
||||
saveState(sessionKey, { runId: newRunId, spans: {} });
|
||||
saveState(sessionKey, { runId: newRunId, runStartedAt: Date.now(), spans: {} });
|
||||
}
|
||||
|
||||
enqueue(buildEnvelope(FRAMEWORK, HOST, 'run.start', sessionKey, {
|
||||
@@ -508,11 +509,11 @@ async function handleNotification(input: Dict) {
|
||||
const notificationType = pickString(input.notification_type, input.type);
|
||||
const usage = getUsage(input);
|
||||
const contextWindow = getContextWindow(input);
|
||||
const duration = pickNumber(input.duration_ms, input.elapsed_ms);
|
||||
|
||||
if (notificationType === 'Done' || notificationType === 'success') {
|
||||
const state = sessionKey ? loadState(sessionKey) : { spans: {} };
|
||||
const runId = sessionKey ? (activeRuns.get(sessionKey) || state.runId) : undefined;
|
||||
const duration = pickNumber(input.duration_ms, input.elapsed_ms) ?? (state.runStartedAt ? Date.now() - state.runStartedAt : undefined);
|
||||
|
||||
if (runId) {
|
||||
enqueue(buildEnvelope(FRAMEWORK, HOST, 'run.end', sessionKey, {
|
||||
|
||||
Reference in New Issue
Block a user