refactor: extract diagnostic session classifier

This commit is contained in:
Peter Steinberger
2026-05-02 00:15:17 +01:00
parent e38fcb254b
commit f8a454e95e
3 changed files with 172 additions and 69 deletions

View File

@@ -0,0 +1,98 @@
import { describe, expect, it } from "vitest";
import { classifySessionAttention } from "./diagnostic-session-attention.js";
describe("classifySessionAttention", () => {
it.each([
{
name: "stale state without queued work",
queueDepth: 0,
activity: {},
expected: {
eventType: "session.stuck",
reason: "stale_session_state",
classification: "stale_session_state",
recoveryEligible: true,
},
},
{
name: "queued stale state without active work",
queueDepth: 1,
activity: {},
expected: {
eventType: "session.stuck",
reason: "queued_work_without_active_run",
classification: "stale_session_state",
recoveryEligible: true,
},
},
{
name: "active embedded run making progress",
queueDepth: 0,
activity: {
activeWorkKind: "embedded_run" as const,
lastProgressAgeMs: 10_000,
},
expected: {
eventType: "session.long_running",
reason: "active_work",
classification: "long_running",
activeWorkKind: "embedded_run",
recoveryEligible: false,
},
},
{
name: "queued behind active work",
queueDepth: 1,
activity: {
activeWorkKind: "embedded_run" as const,
lastProgressAgeMs: 10_000,
},
expected: {
eventType: "session.long_running",
reason: "queued_behind_active_work",
classification: "long_running",
activeWorkKind: "embedded_run",
recoveryEligible: false,
},
},
{
name: "active work without progress",
queueDepth: 0,
activity: {
activeWorkKind: "model_call" as const,
lastProgressAgeMs: 31_000,
},
expected: {
eventType: "session.stalled",
reason: "active_work_without_progress",
classification: "stalled_agent_run",
activeWorkKind: "model_call",
recoveryEligible: false,
},
},
{
name: "blocked tool call",
queueDepth: 0,
activity: {
activeWorkKind: "tool_call" as const,
activeToolAgeMs: 31_000,
lastProgressAgeMs: 31_000,
},
expected: {
eventType: "session.stalled",
reason: "blocked_tool_call",
classification: "blocked_tool_call",
activeWorkKind: "tool_call",
recoveryEligible: false,
},
},
])("$name", ({ activity, expected, queueDepth }) => {
expect(
classifySessionAttention({
queueDepth,
activity,
staleMs: 30_000,
}),
).toEqual(expected);
});
});

View File

@@ -0,0 +1,70 @@
import type { DiagnosticSessionActiveWorkKind } from "../infra/diagnostic-events.js";
import type { DiagnosticSessionActivitySnapshot } from "./diagnostic-run-activity.js";
export type SessionAttentionClassification =
| {
eventType: "session.long_running";
reason: string;
classification: "long_running";
activeWorkKind?: DiagnosticSessionActiveWorkKind;
recoveryEligible: false;
}
| {
eventType: "session.stalled";
reason: string;
classification: "blocked_tool_call" | "stalled_agent_run";
activeWorkKind?: DiagnosticSessionActiveWorkKind;
recoveryEligible: false;
}
| {
eventType: "session.stuck";
reason: string;
classification: "stale_session_state";
activeWorkKind?: undefined;
recoveryEligible: true;
};
export function classifySessionAttention(params: {
queueDepth: number;
activity: DiagnosticSessionActivitySnapshot;
staleMs: number;
}): SessionAttentionClassification {
if (params.activity.activeWorkKind) {
if (
params.activity.activeWorkKind === "tool_call" &&
(params.activity.activeToolAgeMs ?? 0) > params.staleMs &&
(params.activity.lastProgressAgeMs ?? 0) > params.staleMs
) {
return {
eventType: "session.stalled",
reason: "blocked_tool_call",
classification: "blocked_tool_call",
activeWorkKind: params.activity.activeWorkKind,
recoveryEligible: false,
};
}
if ((params.activity.lastProgressAgeMs ?? 0) > params.staleMs) {
return {
eventType: "session.stalled",
reason: "active_work_without_progress",
classification: "stalled_agent_run",
activeWorkKind: params.activity.activeWorkKind,
recoveryEligible: false,
};
}
return {
eventType: "session.long_running",
reason: params.queueDepth > 0 ? "queued_behind_active_work" : "active_work",
classification: "long_running",
activeWorkKind: params.activity.activeWorkKind,
recoveryEligible: false,
};
}
return {
eventType: "session.stuck",
reason: params.queueDepth > 0 ? "queued_work_without_active_run" : "stale_session_state",
classification: "stale_session_state",
recoveryEligible: true,
};
}

View File

@@ -5,7 +5,6 @@ import {
areDiagnosticsEnabledForProcess,
emitDiagnosticEvent,
isDiagnosticsEnabled,
type DiagnosticSessionActiveWorkKind,
type DiagnosticLivenessWarningReason,
} from "../infra/diagnostic-events.js";
import { emitDiagnosticMemorySample, resetDiagnosticMemoryForTest } from "./diagnostic-memory.js";
@@ -20,6 +19,10 @@ import {
markDiagnosticActivity as markActivity,
resetDiagnosticActivityForTest,
} from "./diagnostic-runtime.js";
import {
classifySessionAttention,
type SessionAttentionClassification,
} from "./diagnostic-session-attention.js";
import {
diagnosticSessionStates,
getDiagnosticSessionState,
@@ -156,74 +159,6 @@ function hasRecentDiagnosticActivity(now: number): boolean {
return lastActivityAt > 0 && now - lastActivityAt <= RECENT_DIAGNOSTIC_ACTIVITY_MS;
}
type SessionAttentionClassification =
| {
eventType: "session.long_running";
reason: string;
classification: "long_running";
activeWorkKind?: DiagnosticSessionActiveWorkKind;
recoveryEligible: false;
}
| {
eventType: "session.stalled";
reason: string;
classification: "blocked_tool_call" | "stalled_agent_run";
activeWorkKind?: DiagnosticSessionActiveWorkKind;
recoveryEligible: false;
}
| {
eventType: "session.stuck";
reason: string;
classification: "stale_session_state";
activeWorkKind?: undefined;
recoveryEligible: true;
};
function classifySessionAttention(params: {
queueDepth: number;
activity: DiagnosticSessionActivitySnapshot;
staleMs: number;
}): SessionAttentionClassification {
if (params.activity.activeWorkKind) {
if (
params.activity.activeWorkKind === "tool_call" &&
(params.activity.activeToolAgeMs ?? 0) > params.staleMs &&
(params.activity.lastProgressAgeMs ?? 0) > params.staleMs
) {
return {
eventType: "session.stalled",
reason: "blocked_tool_call",
classification: "blocked_tool_call",
activeWorkKind: params.activity.activeWorkKind,
recoveryEligible: false,
};
}
if ((params.activity.lastProgressAgeMs ?? 0) > params.staleMs) {
return {
eventType: "session.stalled",
reason: "active_work_without_progress",
classification: "stalled_agent_run",
activeWorkKind: params.activity.activeWorkKind,
recoveryEligible: false,
};
}
return {
eventType: "session.long_running",
reason: params.queueDepth > 0 ? "queued_behind_active_work" : "active_work",
classification: "long_running",
activeWorkKind: params.activity.activeWorkKind,
recoveryEligible: false,
};
}
return {
eventType: "session.stuck",
reason: params.queueDepth > 0 ? "queued_work_without_active_run" : "stale_session_state",
classification: "stale_session_state",
recoveryEligible: true,
};
}
function roundDiagnosticMetric(value: number, digits = 3): number {
if (!Number.isFinite(value)) {
return 0;