Skip to content

Commit 0679262

Browse files
committed
fix(recovery): reset command lane when abort leaves pre-existing task blocked
1 parent 301213a commit 0679262

2 files changed

Lines changed: 50 additions & 2 deletions

File tree

src/logging/diagnostic-stuck-session-recovery.runtime.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,40 @@ describe("stuck session recovery", () => {
259259
expect(mocks.resetCommandLane).not.toHaveBeenCalled();
260260
});
261261

262+
it("releases the session lane when abort+drain succeeds but the lane task is still executing", async () => {
263+
mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue("session-1");
264+
mocks.abortEmbeddedAgentRun.mockReturnValue(true);
265+
mocks.waitForEmbeddedAgentRunEnd.mockResolvedValue(true);
266+
mocks.resetCommandLane.mockReturnValue(1);
267+
mocks.getCommandLaneActiveTaskIds.mockReturnValue([101]).mockReturnValue([101]);
268+
mocks.getCommandLaneSnapshot.mockReturnValue({
269+
lane: "session:agent:main:main",
270+
queuedCount: 0,
271+
activeCount: 1,
272+
maxConcurrent: 1,
273+
draining: false,
274+
generation: 0,
275+
});
276+
277+
const outcome = await recoverStuckDiagnosticSession({
278+
sessionId: "session-1",
279+
sessionKey: "agent:main:main",
280+
ageMs: 720_000,
281+
queueDepth: 0,
282+
allowActiveAbort: true,
283+
});
284+
285+
expect(outcome).toMatchObject({
286+
status: "aborted",
287+
action: "abort_embedded_run",
288+
aborted: true,
289+
drained: true,
290+
forceCleared: false,
291+
released: 1,
292+
});
293+
expect(mocks.resetCommandLane).toHaveBeenCalledWith("session:agent:main:main");
294+
});
295+
262296
it("keeps the lane when a fresh turn started during the abort even with lane-queued work", async () => {
263297
mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue("session-fresh-queued");
264298
mocks.abortEmbeddedAgentRun.mockReturnValue(true);

src/logging/diagnostic-stuck-session-recovery.runtime.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,8 @@ export async function recoverStuckDiagnosticSession(
252252
}
253253
}
254254

255-
const queuedCount = sessionLane ? getCommandLaneSnapshot(sessionLane).queuedCount : 0;
255+
const postAbortLaneSnapshot = sessionLane ? getCommandLaneSnapshot(sessionLane) : undefined;
256+
const queuedCount = postAbortLaneSnapshot?.queuedCount ?? 0;
256257
// A task id active now but not before the abort means the lane already
257258
// unwedged and pumped fresh work; resetting it would double-run the lane.
258259
const laneStartedFreshTask =
@@ -261,10 +262,23 @@ export async function recoverStuckDiagnosticSession(
261262
// Queued turns ride the session queue (params.queueDepth), not only the lane
262263
// queue; without this signal a cleanly aborted wedged lane never resets.
263264
const hasQueuedSessionWork = (params.queueDepth ?? 0) > 0;
265+
// A run may have been aborted+drained from the embedded-run registry while its
266+
// underlying command-lane task is still executing (e.g. waitForCompactionRetry
267+
// or in-flight tool execution). If the lane is still blocked by pre-existing
268+
// active tasks we must reset it so queued work can drain.
269+
const laneStillBlockedByPreExistingTask =
270+
!laneStartedFreshTask &&
271+
postAbortLaneSnapshot !== undefined &&
272+
postAbortLaneSnapshot.activeCount > 0;
264273
const released =
265274
sessionLane &&
266275
!laneStartedFreshTask &&
267-
(queuedCount > 0 || hasQueuedSessionWork || !activeSessionId || !aborted || !drained)
276+
(queuedCount > 0 ||
277+
hasQueuedSessionWork ||
278+
!activeSessionId ||
279+
!aborted ||
280+
!drained ||
281+
laneStillBlockedByPreExistingTask)
268282
? resetCommandLane(sessionLane)
269283
: 0;
270284

0 commit comments

Comments
 (0)