fix(todo-continuation-enforcer): track todo state changes for stagnation
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import type { SessionState } from "./types"
|
import type { SessionState, Todo } from "./types"
|
||||||
|
|
||||||
// TTL for idle session state entries (10 minutes)
|
// TTL for idle session state entries (10 minutes)
|
||||||
const SESSION_STATE_TTL_MS = 10 * 60 * 1000
|
const SESSION_STATE_TTL_MS = 10 * 60 * 1000
|
||||||
@@ -8,6 +8,8 @@ const SESSION_STATE_PRUNE_INTERVAL_MS = 2 * 60 * 1000
|
|||||||
interface TrackedSessionState {
|
interface TrackedSessionState {
|
||||||
state: SessionState
|
state: SessionState
|
||||||
lastAccessedAt: number
|
lastAccessedAt: number
|
||||||
|
lastCompletedCount?: number
|
||||||
|
lastTodoStatusSignature?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContinuationProgressUpdate {
|
export interface ContinuationProgressUpdate {
|
||||||
@@ -19,7 +21,7 @@ export interface ContinuationProgressUpdate {
|
|||||||
export interface SessionStateStore {
|
export interface SessionStateStore {
|
||||||
getState: (sessionID: string) => SessionState
|
getState: (sessionID: string) => SessionState
|
||||||
getExistingState: (sessionID: string) => SessionState | undefined
|
getExistingState: (sessionID: string) => SessionState | undefined
|
||||||
trackContinuationProgress: (sessionID: string, incompleteCount: number) => ContinuationProgressUpdate
|
trackContinuationProgress: (sessionID: string, incompleteCount: number, todos?: Todo[]) => ContinuationProgressUpdate
|
||||||
resetContinuationProgress: (sessionID: string) => void
|
resetContinuationProgress: (sessionID: string) => void
|
||||||
cancelCountdown: (sessionID: string) => void
|
cancelCountdown: (sessionID: string) => void
|
||||||
cleanup: (sessionID: string) => void
|
cleanup: (sessionID: string) => void
|
||||||
@@ -27,6 +29,13 @@ export interface SessionStateStore {
|
|||||||
shutdown: () => 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 {
|
export function createSessionStateStore(): SessionStateStore {
|
||||||
const sessions = new Map<string, TrackedSessionState>()
|
const sessions = new Map<string, TrackedSessionState>()
|
||||||
|
|
||||||
@@ -46,19 +55,27 @@ export function createSessionStateStore(): SessionStateStore {
|
|||||||
pruneInterval.unref()
|
pruneInterval.unref()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getState(sessionID: string): SessionState {
|
function getTrackedSession(sessionID: string): TrackedSessionState {
|
||||||
const existing = sessions.get(sessionID)
|
const existing = sessions.get(sessionID)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.lastAccessedAt = Date.now()
|
existing.lastAccessedAt = Date.now()
|
||||||
return existing.state
|
return existing
|
||||||
}
|
}
|
||||||
|
|
||||||
const state: SessionState = {
|
const state: SessionState = {
|
||||||
stagnationCount: 0,
|
stagnationCount: 0,
|
||||||
consecutiveFailures: 0,
|
consecutiveFailures: 0,
|
||||||
}
|
}
|
||||||
sessions.set(sessionID, { state, lastAccessedAt: Date.now() })
|
const trackedSession: TrackedSessionState = {
|
||||||
return state
|
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 {
|
function getExistingState(sessionID: string): SessionState | undefined {
|
||||||
@@ -72,12 +89,30 @@ export function createSessionStateStore(): SessionStateStore {
|
|||||||
|
|
||||||
function trackContinuationProgress(
|
function trackContinuationProgress(
|
||||||
sessionID: string,
|
sessionID: string,
|
||||||
incompleteCount: number
|
incompleteCount: number,
|
||||||
|
todos?: Todo[]
|
||||||
): ContinuationProgressUpdate {
|
): ContinuationProgressUpdate {
|
||||||
const state = getState(sessionID)
|
const trackedSession = getTrackedSession(sessionID)
|
||||||
|
const state = trackedSession.state
|
||||||
const previousIncompleteCount = state.lastIncompleteCount
|
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
|
||||||
|
|
||||||
state.lastIncompleteCount = incompleteCount
|
state.lastIncompleteCount = incompleteCount
|
||||||
|
if (currentCompletedCount !== undefined) {
|
||||||
|
trackedSession.lastCompletedCount = currentCompletedCount
|
||||||
|
}
|
||||||
|
if (currentTodoStatusSignature !== undefined) {
|
||||||
|
trackedSession.lastTodoStatusSignature = currentTodoStatusSignature
|
||||||
|
}
|
||||||
|
|
||||||
if (previousIncompleteCount === undefined) {
|
if (previousIncompleteCount === undefined) {
|
||||||
state.stagnationCount = 0
|
state.stagnationCount = 0
|
||||||
@@ -88,7 +123,7 @@ export function createSessionStateStore(): SessionStateStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (incompleteCount < previousIncompleteCount) {
|
if (incompleteCount < previousIncompleteCount || hasCompletedMoreTodos || hasTodoStatusChanged) {
|
||||||
state.stagnationCount = 0
|
state.stagnationCount = 0
|
||||||
return {
|
return {
|
||||||
previousIncompleteCount,
|
previousIncompleteCount,
|
||||||
@@ -114,11 +149,17 @@ export function createSessionStateStore(): SessionStateStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetContinuationProgress(sessionID: string): void {
|
function resetContinuationProgress(sessionID: string): void {
|
||||||
const state = getExistingState(sessionID)
|
const trackedSession = sessions.get(sessionID)
|
||||||
if (!state) return
|
if (!trackedSession) return
|
||||||
|
|
||||||
|
trackedSession.lastAccessedAt = Date.now()
|
||||||
|
|
||||||
|
const { state } = trackedSession
|
||||||
|
|
||||||
state.lastIncompleteCount = undefined
|
state.lastIncompleteCount = undefined
|
||||||
state.stagnationCount = 0
|
state.stagnationCount = 0
|
||||||
|
trackedSession.lastCompletedCount = undefined
|
||||||
|
trackedSession.lastTodoStatusSignature = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelCountdown(sessionID: string): void {
|
function cancelCountdown(sessionID: string): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user