diff --git a/src/hooks/todo-continuation-enforcer/session-state.test.ts b/src/hooks/todo-continuation-enforcer/session-state.test.ts new file mode 100644 index 000000000..32f2ba627 --- /dev/null +++ b/src/hooks/todo-continuation-enforcer/session-state.test.ts @@ -0,0 +1,49 @@ +import { afterEach, beforeEach, describe, expect, 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) + 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(1) + expect(thirdUpdate.stagnationCount).toBe(2) + }) + + 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) + }) +}) diff --git a/src/hooks/todo-continuation-enforcer/session-state.ts b/src/hooks/todo-continuation-enforcer/session-state.ts index a02a5e5ad..2116a6af0 100644 --- a/src/hooks/todo-continuation-enforcer/session-state.ts +++ b/src/hooks/todo-continuation-enforcer/session-state.ts @@ -10,9 +10,17 @@ interface TrackedSessionState { lastAccessedAt: number } +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) => ContinuationProgressUpdate + resetContinuationProgress: (sessionID: string) => void cancelCountdown: (sessionID: string) => void cleanup: (sessionID: string) => void cancelAllCountdowns: () => void @@ -46,6 +54,7 @@ export function createSessionStateStore(): SessionStateStore { } const state: SessionState = { + stagnationCount: 0, consecutiveFailures: 0, } sessions.set(sessionID, { state, lastAccessedAt: Date.now() }) @@ -61,6 +70,57 @@ export function createSessionStateStore(): SessionStateStore { return undefined } + function trackContinuationProgress( + sessionID: string, + incompleteCount: number + ): ContinuationProgressUpdate { + const state = getState(sessionID) + const previousIncompleteCount = state.lastIncompleteCount + + state.lastIncompleteCount = incompleteCount + + if (previousIncompleteCount === undefined) { + state.stagnationCount = 0 + return { + previousIncompleteCount, + stagnationCount: state.stagnationCount, + hasProgressed: false, + } + } + + if (incompleteCount < previousIncompleteCount) { + state.stagnationCount = 0 + return { + previousIncompleteCount, + stagnationCount: state.stagnationCount, + hasProgressed: true, + } + } + + if (state.lastInjectedAt === undefined) { + return { + previousIncompleteCount, + stagnationCount: state.stagnationCount, + hasProgressed: false, + } + } + + state.stagnationCount += 1 + return { + previousIncompleteCount, + stagnationCount: state.stagnationCount, + hasProgressed: false, + } + } + + function resetContinuationProgress(sessionID: string): void { + const state = getExistingState(sessionID) + if (!state) return + + state.lastIncompleteCount = undefined + state.stagnationCount = 0 + } + function cancelCountdown(sessionID: string): void { const tracked = sessions.get(sessionID) if (!tracked) return @@ -100,6 +160,8 @@ export function createSessionStateStore(): SessionStateStore { return { getState, getExistingState, + trackContinuationProgress, + resetContinuationProgress, cancelCountdown, cleanup, cancelAllCountdowns,