Skip to content

Commit 4e4ea1c

Browse files
gnanam1990clawsweeper[bot]Takhoffman
authored
fix(diagnostics): keep recovery scheduling out of the stuck-session warning backoff (#92752)
Summary: - The branch changes diagnostic stuck/long-running warning backoff so recovery-eligible classifications are still returned during throttled warning ticks and updates the diagnostic tests. - PR surface: Source +17, Tests +48. Total +65 across 2 files. - Reproducibility: yes. Current main source shows logSessionAttention can return undefined during stuck or lon ... g backoff before the heartbeat reaches requestStuckSessionRecovery; I did not run a live QQ gateway replay. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(diagnostics): keep recovery scheduling out of the stuck-session w… Validation: - ClawSweeper review passed for head f61ec3a. - Required merge gates passed before the squash merge. Prepared head SHA: f61ec3a Review: #92752 (comment) Co-authored-by: Gnanam <[email protected]> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <[email protected]>
1 parent b2da129 commit 4e4ea1c

2 files changed

Lines changed: 70 additions & 5 deletions

File tree

src/logging/diagnostic.test.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,16 +468,64 @@ describe("stuck session diagnostics threshold", () => {
468468
);
469469
logSessionStateChange({ sessionId: "s1", sessionKey: "main", state: "processing" });
470470
vi.advanceTimersByTime(91_000);
471+
// One warning emitted (60s); the 90s tick is throttled but still recovers.
471472
expect(events).toHaveLength(1);
472-
expect(recoverStuckSession).toHaveBeenCalledTimes(1);
473+
expect(recoverStuckSession).toHaveBeenCalledTimes(2);
473474

474475
vi.advanceTimersByTime(31_000);
475476
} finally {
476477
unsubscribe();
477478
}
478479

479480
expect(events.map((event) => event.ageMs)).toEqual([60_000, 120_000]);
481+
// Recovery is requested on every heartbeat tick the session stays stuck,
482+
// including the throttled tick at 90s, so it must outpace the warn backoff.
483+
expect(recoverStuckSession).toHaveBeenCalledTimes(3);
484+
});
485+
486+
it("keeps scheduling recovery for a recovery-eligible stuck session while warnings are throttled", () => {
487+
const stuckEvents: Array<{ ageMs?: number }> = [];
488+
const recoveryRequests: Array<{ ageMs?: number }> = [];
489+
const recoverStuckSession = vi.fn();
490+
const unsubscribe = onDiagnosticEvent((event) => {
491+
if (event.type === "session.stuck") {
492+
stuckEvents.push(event);
493+
} else if (event.type === "session.recovery.requested") {
494+
recoveryRequests.push(event);
495+
}
496+
});
497+
try {
498+
startDiagnosticHeartbeat(
499+
{
500+
diagnostics: {
501+
enabled: true,
502+
stuckSessionWarnMs: 30_000,
503+
},
504+
},
505+
{ recoverStuckSession },
506+
);
507+
logSessionStateChange({ sessionId: "s1", sessionKey: "main", state: "processing" });
508+
509+
// First warn tick (60s): emit the stuck warning and request recovery once.
510+
vi.advanceTimersByTime(61_000);
511+
expect(stuckEvents).toHaveLength(1);
512+
expect(recoverStuckSession).toHaveBeenCalledTimes(1);
513+
514+
// Backoff tick (90s): the next warn age is 120s, so the warning is
515+
// throttled. Recovery must still be scheduled because the session is
516+
// recovery-eligible — the warning backoff must not gate recovery.
517+
vi.advanceTimersByTime(30_000);
518+
} finally {
519+
unsubscribe();
520+
}
521+
522+
// Warning stays throttled: still only the single 60s warning.
523+
expect(stuckEvents).toHaveLength(1);
524+
expect(stuckEvents.map((event) => event.ageMs)).toEqual([60_000]);
525+
// Recovery was not suppressed by the warning backoff on the 90s tick.
480526
expect(recoverStuckSession).toHaveBeenCalledTimes(2);
527+
expect(recoveryRequests).toHaveLength(2);
528+
expect(recoveryRequests.map((event) => event.ageMs)).toEqual([60_000, 90_000]);
481529
});
482530

483531
it("reports active sessions as stalled instead of stuck when active work stops progressing", () => {

src/logging/diagnostic.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,15 +1022,23 @@ export function logSessionAttention(
10221022
stuckSessionAbortMs:
10231023
params.abortThresholdMs ?? resolveStalledEmbeddedRunAbortMs(params.thresholdMs),
10241024
});
1025+
// The warning backoff throttles repeated log lines/events only. It must never
1026+
// gate recovery: a recovery-eligible session has to return its classification
1027+
// so the heartbeat can still schedule recovery on every tick.
1028+
let suppressWarning = false;
10251029
if (classification.eventType === "session.stuck") {
10261030
const nextWarnAgeMs =
10271031
state.lastStuckWarnAgeMs === undefined
10281032
? params.thresholdMs
10291033
: Math.max(state.lastStuckWarnAgeMs + params.thresholdMs, state.lastStuckWarnAgeMs * 2);
10301034
if (params.ageMs < nextWarnAgeMs) {
1031-
return undefined;
1035+
if (!recoveryEligible) {
1036+
return undefined;
1037+
}
1038+
suppressWarning = true;
1039+
} else {
1040+
state.lastStuckWarnAgeMs = params.ageMs;
10321041
}
1033-
state.lastStuckWarnAgeMs = params.ageMs;
10341042
}
10351043
if (classification.eventType === "session.long_running") {
10361044
const nextWarnAgeMs =
@@ -1041,9 +1049,18 @@ export function logSessionAttention(
10411049
state.lastLongRunningWarnAgeMs * 2,
10421050
);
10431051
if (params.ageMs < nextWarnAgeMs) {
1044-
return undefined;
1052+
if (!recoveryEligible) {
1053+
return undefined;
1054+
}
1055+
suppressWarning = true;
1056+
} else {
1057+
state.lastLongRunningWarnAgeMs = params.ageMs;
10451058
}
1046-
state.lastLongRunningWarnAgeMs = params.ageMs;
1059+
}
1060+
if (suppressWarning) {
1061+
// Throttled warning, but recovery-eligible: skip the log/event and return
1062+
// the classification so the heartbeat can drive recovery.
1063+
return classification;
10471064
}
10481065
const label =
10491066
classification.eventType === "session.stuck"

0 commit comments

Comments
 (0)