Merge pull request #2370 from code-yeongyu/fix/issue-2322
fix: stop stagnant todo continuation loops
This commit is contained in:
@@ -18,5 +18,6 @@ export const COUNTDOWN_GRACE_PERIOD_MS = 500
|
||||
|
||||
export const ABORT_WINDOW_MS = 3000
|
||||
export const CONTINUATION_COOLDOWN_MS = 5_000
|
||||
export const MAX_STAGNATION_COUNT = 3
|
||||
export const MAX_CONSECUTIVE_FAILURES = 5
|
||||
export const FAILURE_RESET_WINDOW_MS = 5 * 60 * 1000
|
||||
|
||||
@@ -164,6 +164,7 @@ ${todoList}`
|
||||
if (injectionState) {
|
||||
injectionState.inFlight = false
|
||||
injectionState.lastInjectedAt = Date.now()
|
||||
injectionState.awaitingPostInjectionProgressCheck = true
|
||||
injectionState.consecutiveFailures = 0
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "./constants"
|
||||
import { isLastAssistantMessageAborted } from "./abort-detection"
|
||||
import { hasUnansweredQuestion } from "./pending-question-detection"
|
||||
import { shouldStopForStagnation } from "./stagnation-detection"
|
||||
import { getIncompleteCount } from "./todo"
|
||||
import type { MessageInfo, ResolvedMessageInfo, Todo } from "./types"
|
||||
import type { SessionStateStore } from "./session-state"
|
||||
@@ -93,12 +94,14 @@ export async function handleSessionIdle(args: {
|
||||
}
|
||||
|
||||
if (!todos || todos.length === 0) {
|
||||
sessionStateStore.resetContinuationProgress(sessionID)
|
||||
log(`[${HOOK_NAME}] No todos`, { sessionID })
|
||||
return
|
||||
}
|
||||
|
||||
const incompleteCount = getIncompleteCount(todos)
|
||||
if (incompleteCount === 0) {
|
||||
sessionStateStore.resetContinuationProgress(sessionID)
|
||||
log(`[${HOOK_NAME}] All todos complete`, { sessionID, total: todos.length })
|
||||
return
|
||||
}
|
||||
@@ -183,6 +186,11 @@ export async function handleSessionIdle(args: {
|
||||
return
|
||||
}
|
||||
|
||||
const progressUpdate = sessionStateStore.trackContinuationProgress(sessionID, incompleteCount, todos)
|
||||
if (shouldStopForStagnation({ sessionID, incompleteCount, progressUpdate })) {
|
||||
return
|
||||
}
|
||||
|
||||
startCountdown({
|
||||
ctx,
|
||||
sessionID,
|
||||
|
||||
146
src/hooks/todo-continuation-enforcer/session-state.test.ts
Normal file
146
src/hooks/todo-continuation-enforcer/session-state.test.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/// <reference path="../../../bun-test.d.ts" />
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it as test } from "bun:test"
|
||||
|
||||
import { createSessionStateStore, type SessionStateStore } from "./session-state"
|
||||
|
||||
describe("createSessionStateStore", () => {
|
||||
let sessionStateStore: SessionStateStore
|
||||
|
||||
beforeEach(() => {
|
||||
sessionStateStore = createSessionStateStore()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sessionStateStore.shutdown()
|
||||
})
|
||||
|
||||
test("given repeated incomplete counts after a continuation, tracks stagnation", () => {
|
||||
// given
|
||||
const sessionID = "ses-stagnation"
|
||||
const state = sessionStateStore.getState(sessionID)
|
||||
|
||||
// when
|
||||
const firstUpdate = sessionStateStore.trackContinuationProgress(sessionID, 2)
|
||||
state.awaitingPostInjectionProgressCheck = true
|
||||
const secondUpdate = sessionStateStore.trackContinuationProgress(sessionID, 2)
|
||||
state.awaitingPostInjectionProgressCheck = true
|
||||
const thirdUpdate = sessionStateStore.trackContinuationProgress(sessionID, 2)
|
||||
|
||||
// then
|
||||
expect(firstUpdate.stagnationCount).toBe(0)
|
||||
expect(secondUpdate.stagnationCount).toBe(1)
|
||||
expect(thirdUpdate.stagnationCount).toBe(2)
|
||||
})
|
||||
|
||||
test("given injection did not succeed, repeated incomplete counts do not track stagnation", () => {
|
||||
// given
|
||||
const sessionID = "ses-failed-injection"
|
||||
const state = sessionStateStore.getState(sessionID)
|
||||
state.lastInjectedAt = Date.now()
|
||||
|
||||
// when
|
||||
const firstUpdate = sessionStateStore.trackContinuationProgress(sessionID, 2)
|
||||
const secondUpdate = sessionStateStore.trackContinuationProgress(sessionID, 2)
|
||||
const thirdUpdate = sessionStateStore.trackContinuationProgress(sessionID, 2)
|
||||
|
||||
// then
|
||||
expect(firstUpdate.stagnationCount).toBe(0)
|
||||
expect(secondUpdate.stagnationCount).toBe(0)
|
||||
expect(thirdUpdate.stagnationCount).toBe(0)
|
||||
})
|
||||
|
||||
test("given incomplete count decreases, resets stagnation tracking", () => {
|
||||
// given
|
||||
const sessionID = "ses-progress-reset"
|
||||
const state = sessionStateStore.getState(sessionID)
|
||||
state.lastInjectedAt = Date.now()
|
||||
sessionStateStore.trackContinuationProgress(sessionID, 3)
|
||||
sessionStateStore.trackContinuationProgress(sessionID, 3)
|
||||
|
||||
// when
|
||||
const progressUpdate = sessionStateStore.trackContinuationProgress(sessionID, 2)
|
||||
|
||||
// then
|
||||
expect(progressUpdate.hasProgressed).toBe(true)
|
||||
expect(progressUpdate.stagnationCount).toBe(0)
|
||||
expect(sessionStateStore.getState(sessionID).lastIncompleteCount).toBe(2)
|
||||
})
|
||||
|
||||
test("given one todo completes while another is added, resets stagnation even when incomplete count stays the same", () => {
|
||||
// given
|
||||
const sessionID = "ses-completion-with-addition"
|
||||
const state = sessionStateStore.getState(sessionID)
|
||||
state.lastInjectedAt = Date.now()
|
||||
const initialTodos = [
|
||||
{ id: "1", content: "Task 1", status: "pending", priority: "high" },
|
||||
{ id: "2", content: "Task 2", status: "pending", priority: "medium" },
|
||||
]
|
||||
const progressedTodos = [
|
||||
{ id: "1", content: "Task 1", status: "completed", priority: "high" },
|
||||
{ id: "2", content: "Task 2", status: "pending", priority: "medium" },
|
||||
{ id: "3", content: "Task 3", status: "pending", priority: "low" },
|
||||
]
|
||||
sessionStateStore.trackContinuationProgress(sessionID, 2, initialTodos)
|
||||
sessionStateStore.trackContinuationProgress(sessionID, 2, initialTodos)
|
||||
|
||||
// when
|
||||
const progressUpdate = sessionStateStore.trackContinuationProgress(sessionID, 2, progressedTodos)
|
||||
|
||||
// then
|
||||
expect(progressUpdate.hasProgressed).toBe(true)
|
||||
expect(progressUpdate.stagnationCount).toBe(0)
|
||||
})
|
||||
|
||||
test("given todo status changes without count changes, treats it as progress", () => {
|
||||
// given
|
||||
const sessionID = "ses-status-change-progress"
|
||||
const state = sessionStateStore.getState(sessionID)
|
||||
state.lastInjectedAt = Date.now()
|
||||
const initialTodos = [
|
||||
{ id: "1", content: "Task 1", status: "pending", priority: "high" },
|
||||
{ id: "2", content: "Task 2", status: "pending", priority: "medium" },
|
||||
]
|
||||
const progressedTodos = [
|
||||
{ id: "1", content: "Task 1", status: "in_progress", priority: "high" },
|
||||
{ id: "2", content: "Task 2", status: "pending", priority: "medium" },
|
||||
]
|
||||
sessionStateStore.trackContinuationProgress(sessionID, 2, initialTodos)
|
||||
sessionStateStore.trackContinuationProgress(sessionID, 2, initialTodos)
|
||||
|
||||
// when
|
||||
const progressUpdate = sessionStateStore.trackContinuationProgress(sessionID, 2, progressedTodos)
|
||||
|
||||
// then
|
||||
expect(progressUpdate.hasProgressed).toBe(true)
|
||||
expect(progressUpdate.stagnationCount).toBe(0)
|
||||
})
|
||||
|
||||
test("given progress resumes after stagnation, restarts the stagnation count from zero", () => {
|
||||
// given
|
||||
const sessionID = "ses-progress-restarts-stagnation"
|
||||
const state = sessionStateStore.getState(sessionID)
|
||||
state.lastInjectedAt = Date.now()
|
||||
const initialTodos = [
|
||||
{ id: "1", content: "Task 1", status: "pending", priority: "high" },
|
||||
{ id: "2", content: "Task 2", status: "pending", priority: "medium" },
|
||||
]
|
||||
const progressedTodos = [
|
||||
{ id: "1", content: "Task 1", status: "in_progress", priority: "high" },
|
||||
{ id: "2", content: "Task 2", status: "pending", priority: "medium" },
|
||||
]
|
||||
sessionStateStore.trackContinuationProgress(sessionID, 2, initialTodos)
|
||||
state.awaitingPostInjectionProgressCheck = true
|
||||
sessionStateStore.trackContinuationProgress(sessionID, 2, initialTodos)
|
||||
state.awaitingPostInjectionProgressCheck = true
|
||||
sessionStateStore.trackContinuationProgress(sessionID, 2, progressedTodos)
|
||||
|
||||
// when
|
||||
state.awaitingPostInjectionProgressCheck = true
|
||||
const stagnatedAgainUpdate = sessionStateStore.trackContinuationProgress(sessionID, 2, progressedTodos)
|
||||
|
||||
// then
|
||||
expect(stagnatedAgainUpdate.hasProgressed).toBe(false)
|
||||
expect(stagnatedAgainUpdate.stagnationCount).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SessionState } from "./types"
|
||||
import type { SessionState, Todo } from "./types"
|
||||
|
||||
// TTL for idle session state entries (10 minutes)
|
||||
const SESSION_STATE_TTL_MS = 10 * 60 * 1000
|
||||
@@ -8,17 +8,34 @@ const SESSION_STATE_PRUNE_INTERVAL_MS = 2 * 60 * 1000
|
||||
interface TrackedSessionState {
|
||||
state: SessionState
|
||||
lastAccessedAt: number
|
||||
lastCompletedCount?: number
|
||||
lastTodoStatusSignature?: string
|
||||
}
|
||||
|
||||
export interface ContinuationProgressUpdate {
|
||||
previousIncompleteCount?: number
|
||||
stagnationCount: number
|
||||
hasProgressed: boolean
|
||||
}
|
||||
|
||||
export interface SessionStateStore {
|
||||
getState: (sessionID: string) => SessionState
|
||||
getExistingState: (sessionID: string) => SessionState | undefined
|
||||
trackContinuationProgress: (sessionID: string, incompleteCount: number, todos?: Todo[]) => ContinuationProgressUpdate
|
||||
resetContinuationProgress: (sessionID: string) => void
|
||||
cancelCountdown: (sessionID: string) => void
|
||||
cleanup: (sessionID: string) => void
|
||||
cancelAllCountdowns: () => void
|
||||
shutdown: () => void
|
||||
}
|
||||
|
||||
function getTodoStatusSignature(todos: Todo[]): string {
|
||||
return todos
|
||||
.map((todo) => `${todo.id ?? `${todo.content}:${todo.priority}`}:${todo.status}`)
|
||||
.sort()
|
||||
.join("|")
|
||||
}
|
||||
|
||||
export function createSessionStateStore(): SessionStateStore {
|
||||
const sessions = new Map<string, TrackedSessionState>()
|
||||
|
||||
@@ -38,18 +55,27 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
pruneInterval.unref()
|
||||
}
|
||||
|
||||
function getState(sessionID: string): SessionState {
|
||||
function getTrackedSession(sessionID: string): TrackedSessionState {
|
||||
const existing = sessions.get(sessionID)
|
||||
if (existing) {
|
||||
existing.lastAccessedAt = Date.now()
|
||||
return existing.state
|
||||
return existing
|
||||
}
|
||||
|
||||
const state: SessionState = {
|
||||
stagnationCount: 0,
|
||||
consecutiveFailures: 0,
|
||||
}
|
||||
sessions.set(sessionID, { state, lastAccessedAt: Date.now() })
|
||||
return state
|
||||
const trackedSession: TrackedSessionState = {
|
||||
state,
|
||||
lastAccessedAt: Date.now(),
|
||||
}
|
||||
sessions.set(sessionID, trackedSession)
|
||||
return trackedSession
|
||||
}
|
||||
|
||||
function getState(sessionID: string): SessionState {
|
||||
return getTrackedSession(sessionID).state
|
||||
}
|
||||
|
||||
function getExistingState(sessionID: string): SessionState | undefined {
|
||||
@@ -61,6 +87,85 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
return undefined
|
||||
}
|
||||
|
||||
function trackContinuationProgress(
|
||||
sessionID: string,
|
||||
incompleteCount: number,
|
||||
todos?: Todo[]
|
||||
): ContinuationProgressUpdate {
|
||||
const trackedSession = getTrackedSession(sessionID)
|
||||
const state = trackedSession.state
|
||||
const previousIncompleteCount = state.lastIncompleteCount
|
||||
const currentCompletedCount = todos?.filter((todo) => todo.status === "completed").length
|
||||
const currentTodoStatusSignature = todos ? getTodoStatusSignature(todos) : undefined
|
||||
const hasCompletedMoreTodos =
|
||||
currentCompletedCount !== undefined
|
||||
&& trackedSession.lastCompletedCount !== undefined
|
||||
&& currentCompletedCount > trackedSession.lastCompletedCount
|
||||
const hasTodoStatusChanged =
|
||||
currentTodoStatusSignature !== undefined
|
||||
&& trackedSession.lastTodoStatusSignature !== undefined
|
||||
&& currentTodoStatusSignature !== trackedSession.lastTodoStatusSignature
|
||||
const hadSuccessfulInjectionAwaitingProgressCheck = state.awaitingPostInjectionProgressCheck === true
|
||||
|
||||
state.lastIncompleteCount = incompleteCount
|
||||
if (currentCompletedCount !== undefined) {
|
||||
trackedSession.lastCompletedCount = currentCompletedCount
|
||||
}
|
||||
if (currentTodoStatusSignature !== undefined) {
|
||||
trackedSession.lastTodoStatusSignature = currentTodoStatusSignature
|
||||
}
|
||||
|
||||
if (previousIncompleteCount === undefined) {
|
||||
state.stagnationCount = 0
|
||||
return {
|
||||
previousIncompleteCount,
|
||||
stagnationCount: state.stagnationCount,
|
||||
hasProgressed: false,
|
||||
}
|
||||
}
|
||||
|
||||
if (incompleteCount < previousIncompleteCount || hasCompletedMoreTodos || hasTodoStatusChanged) {
|
||||
state.stagnationCount = 0
|
||||
state.awaitingPostInjectionProgressCheck = false
|
||||
return {
|
||||
previousIncompleteCount,
|
||||
stagnationCount: state.stagnationCount,
|
||||
hasProgressed: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (!hadSuccessfulInjectionAwaitingProgressCheck) {
|
||||
return {
|
||||
previousIncompleteCount,
|
||||
stagnationCount: state.stagnationCount,
|
||||
hasProgressed: false,
|
||||
}
|
||||
}
|
||||
|
||||
state.awaitingPostInjectionProgressCheck = false
|
||||
state.stagnationCount += 1
|
||||
return {
|
||||
previousIncompleteCount,
|
||||
stagnationCount: state.stagnationCount,
|
||||
hasProgressed: false,
|
||||
}
|
||||
}
|
||||
|
||||
function resetContinuationProgress(sessionID: string): void {
|
||||
const trackedSession = sessions.get(sessionID)
|
||||
if (!trackedSession) return
|
||||
|
||||
trackedSession.lastAccessedAt = Date.now()
|
||||
|
||||
const { state } = trackedSession
|
||||
|
||||
state.lastIncompleteCount = undefined
|
||||
state.stagnationCount = 0
|
||||
state.awaitingPostInjectionProgressCheck = false
|
||||
trackedSession.lastCompletedCount = undefined
|
||||
trackedSession.lastTodoStatusSignature = undefined
|
||||
}
|
||||
|
||||
function cancelCountdown(sessionID: string): void {
|
||||
const tracked = sessions.get(sessionID)
|
||||
if (!tracked) return
|
||||
@@ -100,6 +205,8 @@ export function createSessionStateStore(): SessionStateStore {
|
||||
return {
|
||||
getState,
|
||||
getExistingState,
|
||||
trackContinuationProgress,
|
||||
resetContinuationProgress,
|
||||
cancelCountdown,
|
||||
cleanup,
|
||||
cancelAllCountdowns,
|
||||
|
||||
33
src/hooks/todo-continuation-enforcer/stagnation-detection.ts
Normal file
33
src/hooks/todo-continuation-enforcer/stagnation-detection.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { log } from "../../shared/logger"
|
||||
|
||||
import { HOOK_NAME, MAX_STAGNATION_COUNT } from "./constants"
|
||||
import type { ContinuationProgressUpdate } from "./session-state"
|
||||
|
||||
export function shouldStopForStagnation(args: {
|
||||
sessionID: string
|
||||
incompleteCount: number
|
||||
progressUpdate: ContinuationProgressUpdate
|
||||
}): boolean {
|
||||
const { sessionID, incompleteCount, progressUpdate } = args
|
||||
|
||||
if (progressUpdate.hasProgressed) {
|
||||
log(`[${HOOK_NAME}] Progress detected: reset stagnation count`, {
|
||||
sessionID,
|
||||
previousIncompleteCount: progressUpdate.previousIncompleteCount,
|
||||
incompleteCount,
|
||||
})
|
||||
}
|
||||
|
||||
if (progressUpdate.stagnationCount < MAX_STAGNATION_COUNT) {
|
||||
return false
|
||||
}
|
||||
|
||||
log(`[${HOOK_NAME}] Skipped: todo continuation stagnated`, {
|
||||
sessionID,
|
||||
incompleteCount,
|
||||
previousIncompleteCount: progressUpdate.previousIncompleteCount,
|
||||
stagnationCount: progressUpdate.stagnationCount,
|
||||
maxStagnationCount: MAX_STAGNATION_COUNT,
|
||||
})
|
||||
return true
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CONTINUATION_COOLDOWN_MS,
|
||||
FAILURE_RESET_WINDOW_MS,
|
||||
MAX_CONSECUTIVE_FAILURES,
|
||||
MAX_STAGNATION_COUNT,
|
||||
} from "./constants"
|
||||
|
||||
type TimerCallback = (...args: any[]) => void
|
||||
@@ -626,6 +627,57 @@ describe("todo-continuation-enforcer", () => {
|
||||
const sessionID = "main-max-consecutive-failures"
|
||||
setMainSession(sessionID)
|
||||
const mockInput = createMockPluginInput()
|
||||
const incompleteCounts = [5, 4, 5, 4, 5, 4]
|
||||
let todoCallCount = 0
|
||||
mockInput.client.session.todo = async () => {
|
||||
const countIndex = Math.min(Math.floor(todoCallCount / 2), incompleteCounts.length - 1)
|
||||
const incompleteCount = incompleteCounts[countIndex] ?? incompleteCounts[incompleteCounts.length - 1] ?? 1
|
||||
todoCallCount += 1
|
||||
return {
|
||||
data: Array.from({ length: incompleteCount }, (_, index) => ({
|
||||
id: String(index + 1),
|
||||
content: `Task ${index + 1}`,
|
||||
status: "pending",
|
||||
priority: "high",
|
||||
})),
|
||||
}
|
||||
}
|
||||
mockInput.client.session.promptAsync = async (opts: PromptRequestOptions) => {
|
||||
promptCalls.push({
|
||||
sessionID: opts.path.id,
|
||||
agent: opts.body.agent,
|
||||
model: opts.body.model,
|
||||
text: opts.body.parts[0].text,
|
||||
})
|
||||
throw new Error("simulated auth failure")
|
||||
}
|
||||
const hook = createTodoContinuationEnforcer(mockInput, {})
|
||||
|
||||
//#when
|
||||
for (let index = 0; index < MAX_CONSECUTIVE_FAILURES; index++) {
|
||||
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
|
||||
await fakeTimers.advanceBy(2500, true)
|
||||
if (index < MAX_CONSECUTIVE_FAILURES - 1) {
|
||||
await fakeTimers.advanceClockBy(1_000_000)
|
||||
}
|
||||
}
|
||||
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
|
||||
await fakeTimers.advanceBy(2500, true)
|
||||
|
||||
//#then
|
||||
expect(promptCalls).toHaveLength(MAX_CONSECUTIVE_FAILURES)
|
||||
}, { timeout: 30000 })
|
||||
|
||||
test("should not stop retries early for unchanged todos when injections keep failing", async () => {
|
||||
//#given
|
||||
const sessionID = "main-unchanged-todos-max-failures"
|
||||
setMainSession(sessionID)
|
||||
const mockInput = createMockPluginInput()
|
||||
mockInput.client.session.todo = async () => ({
|
||||
data: [
|
||||
{ id: "1", content: "Task 1", status: "pending", priority: "high" },
|
||||
],
|
||||
})
|
||||
mockInput.client.session.promptAsync = async (opts: PromptRequestOptions) => {
|
||||
promptCalls.push({
|
||||
sessionID: opts.path.id,
|
||||
@@ -657,6 +709,21 @@ describe("todo-continuation-enforcer", () => {
|
||||
const sessionID = "main-recovery-after-max-failures"
|
||||
setMainSession(sessionID)
|
||||
const mockInput = createMockPluginInput()
|
||||
const incompleteCounts = [5, 4, 5, 4, 5, 4, 5]
|
||||
let todoCallCount = 0
|
||||
mockInput.client.session.todo = async () => {
|
||||
const countIndex = Math.min(Math.floor(todoCallCount / 2), incompleteCounts.length - 1)
|
||||
const incompleteCount = incompleteCounts[countIndex] ?? incompleteCounts[incompleteCounts.length - 1] ?? 1
|
||||
todoCallCount += 1
|
||||
return {
|
||||
data: Array.from({ length: incompleteCount }, (_, index) => ({
|
||||
id: String(index + 1),
|
||||
content: `Task ${index + 1}`,
|
||||
status: "pending",
|
||||
priority: "high",
|
||||
})),
|
||||
}
|
||||
}
|
||||
mockInput.client.session.promptAsync = async (opts: PromptRequestOptions) => {
|
||||
promptCalls.push({
|
||||
sessionID: opts.path.id,
|
||||
@@ -753,7 +820,7 @@ describe("todo-continuation-enforcer", () => {
|
||||
expect(promptCalls).toHaveLength(3)
|
||||
}, { timeout: 30000 })
|
||||
|
||||
test("should keep injecting even when todos remain unchanged across cycles", async () => {
|
||||
test("should stop injecting after max stagnation cycles when todos remain unchanged across cycles", async () => {
|
||||
//#given
|
||||
const sessionID = "main-no-stagnation-cap"
|
||||
setMainSession(sessionID)
|
||||
@@ -784,8 +851,8 @@ describe("todo-continuation-enforcer", () => {
|
||||
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
|
||||
await fakeTimers.advanceBy(2500, true)
|
||||
|
||||
//#then — all 5 injections should fire (no stagnation cap)
|
||||
expect(promptCalls).toHaveLength(5)
|
||||
// then
|
||||
expect(promptCalls).toHaveLength(MAX_STAGNATION_COUNT)
|
||||
}, { timeout: 60000 })
|
||||
|
||||
test("should skip idle handling while injection is in flight", async () => {
|
||||
|
||||
@@ -27,8 +27,11 @@ export interface SessionState {
|
||||
isRecovering?: boolean
|
||||
countdownStartedAt?: number
|
||||
abortDetectedAt?: number
|
||||
lastIncompleteCount?: number
|
||||
lastInjectedAt?: number
|
||||
awaitingPostInjectionProgressCheck?: boolean
|
||||
inFlight?: boolean
|
||||
stagnationCount: number
|
||||
consecutiveFailures: number
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user