mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
refactor: extract diagnostic session classifier
This commit is contained in:
98
src/logging/diagnostic-session-attention.test.ts
Normal file
98
src/logging/diagnostic-session-attention.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
70
src/logging/diagnostic-session-attention.ts
Normal file
70
src/logging/diagnostic-session-attention.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user