fix(tmux-subagent): cap stale close retries
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -196,25 +196,30 @@ export class TmuxSessionManager {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextRetryCount = tracked.closeRetryCount + 1
|
const currentTracked = this.sessions.get(tracked.sessionId)
|
||||||
if (nextRetryCount >= MAX_CLOSE_RETRY_COUNT) {
|
if (!currentTracked || !currentTracked.closePending) {
|
||||||
log("[tmux-session-manager] force removing close-pending session after failed retry", {
|
|
||||||
sessionId: tracked.sessionId,
|
|
||||||
paneId: tracked.paneId,
|
|
||||||
closeRetryCount: nextRetryCount,
|
|
||||||
})
|
|
||||||
this.removeTrackedSession(tracked.sessionId)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sessions.set(tracked.sessionId, {
|
const nextRetryCount = currentTracked.closeRetryCount + 1
|
||||||
...tracked,
|
if (nextRetryCount >= MAX_CLOSE_RETRY_COUNT) {
|
||||||
|
log("[tmux-session-manager] force removing close-pending session after failed retry", {
|
||||||
|
sessionId: currentTracked.sessionId,
|
||||||
|
paneId: currentTracked.paneId,
|
||||||
|
closeRetryCount: nextRetryCount,
|
||||||
|
})
|
||||||
|
this.removeTrackedSession(currentTracked.sessionId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sessions.set(currentTracked.sessionId, {
|
||||||
|
...currentTracked,
|
||||||
closePending: true,
|
closePending: true,
|
||||||
closeRetryCount: nextRetryCount,
|
closeRetryCount: nextRetryCount,
|
||||||
})
|
})
|
||||||
log("[tmux-session-manager] retried close failed", {
|
log("[tmux-session-manager] retried close failed", {
|
||||||
sessionId: tracked.sessionId,
|
sessionId: currentTracked.sessionId,
|
||||||
paneId: tracked.paneId,
|
paneId: currentTracked.paneId,
|
||||||
closeRetryCount: nextRetryCount,
|
closeRetryCount: nextRetryCount,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -642,6 +647,16 @@ export class TmuxSessionManager {
|
|||||||
const tracked = this.sessions.get(sessionId)
|
const tracked = this.sessions.get(sessionId)
|
||||||
if (!tracked) return
|
if (!tracked) return
|
||||||
|
|
||||||
|
if (tracked.closePending && tracked.closeRetryCount >= MAX_CLOSE_RETRY_COUNT) {
|
||||||
|
log("[tmux-session-manager] force removing close-pending session after max retries", {
|
||||||
|
sessionId,
|
||||||
|
paneId: tracked.paneId,
|
||||||
|
closeRetryCount: tracked.closeRetryCount,
|
||||||
|
})
|
||||||
|
this.removeTrackedSession(sessionId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
log("[tmux-session-manager] closing session pane", {
|
log("[tmux-session-manager] closing session pane", {
|
||||||
sessionId,
|
sessionId,
|
||||||
paneId: tracked.paneId,
|
paneId: tracked.paneId,
|
||||||
|
|||||||
@@ -134,6 +134,15 @@ function getRetryPendingCloses(target: object): () => Promise<void> {
|
|||||||
return retryPendingCloses.bind(target)
|
return retryPendingCloses.bind(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCloseSessionById(target: object): (sessionId: string) => Promise<void> {
|
||||||
|
const closeSessionById = Reflect.get(target, "closeSessionById")
|
||||||
|
if (typeof closeSessionById !== "function") {
|
||||||
|
throw new Error("Expected closeSessionById method")
|
||||||
|
}
|
||||||
|
|
||||||
|
return closeSessionById.bind(target)
|
||||||
|
}
|
||||||
|
|
||||||
function createManager(
|
function createManager(
|
||||||
TmuxSessionManager: typeof import("./manager").TmuxSessionManager,
|
TmuxSessionManager: typeof import("./manager").TmuxSessionManager,
|
||||||
): import("./manager").TmuxSessionManager {
|
): import("./manager").TmuxSessionManager {
|
||||||
@@ -219,4 +228,44 @@ describe("TmuxSessionManager zombie pane handling", () => {
|
|||||||
expect(mockQueryWindowState).not.toHaveBeenCalled()
|
expect(mockQueryWindowState).not.toHaveBeenCalled()
|
||||||
expect(mockExecuteAction).not.toHaveBeenCalled()
|
expect(mockExecuteAction).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("#given session with closePending true and closeRetryCount >= 3 #when closeSessionById called #then session is force-removed without retrying close", async () => {
|
||||||
|
// given
|
||||||
|
const { TmuxSessionManager } = await import("./manager")
|
||||||
|
const manager = createManager(TmuxSessionManager)
|
||||||
|
const sessions = getTrackedSessions(manager)
|
||||||
|
sessions.set(
|
||||||
|
"ses_pending",
|
||||||
|
createTrackedSession({ closePending: true, closeRetryCount: 3 }),
|
||||||
|
)
|
||||||
|
|
||||||
|
// when
|
||||||
|
await getCloseSessionById(manager)("ses_pending")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(sessions.has("ses_pending")).toBe(false)
|
||||||
|
expect(mockQueryWindowState).not.toHaveBeenCalled()
|
||||||
|
expect(mockExecuteAction).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given close-pending session removed during async close #when retryPendingCloses fails #then it does not resurrect stale session state", async () => {
|
||||||
|
// given
|
||||||
|
const { TmuxSessionManager } = await import("./manager")
|
||||||
|
const manager = createManager(TmuxSessionManager)
|
||||||
|
const sessions = getTrackedSessions(manager)
|
||||||
|
sessions.set(
|
||||||
|
"ses_pending",
|
||||||
|
createTrackedSession({ closePending: true, closeRetryCount: 0 }),
|
||||||
|
)
|
||||||
|
mockExecuteAction.mockImplementationOnce(async () => {
|
||||||
|
sessions.delete("ses_pending")
|
||||||
|
return { success: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
// when
|
||||||
|
await getRetryPendingCloses(manager)()
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(sessions.has("ses_pending")).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user