From fbaeb032c07c1e279548916edb271149978eb11c Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 28 Apr 2026 18:00:47 +0900 Subject: [PATCH] fix(todo-continuation): avoid duplicate progress reset Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../idle-event.test.ts | 99 +++++++++++++++++++ .../todo-continuation-enforcer/idle-event.ts | 2 - 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 src/hooks/todo-continuation-enforcer/idle-event.test.ts diff --git a/src/hooks/todo-continuation-enforcer/idle-event.test.ts b/src/hooks/todo-continuation-enforcer/idle-event.test.ts new file mode 100644 index 000000000..3e9746fdc --- /dev/null +++ b/src/hooks/todo-continuation-enforcer/idle-event.test.ts @@ -0,0 +1,99 @@ +/// + +import { describe, expect, it } from "bun:test" + +import { handleSessionIdle } from "./idle-event" +import type { SessionStateStore } from "./session-state" +import type { ContinuationProgressUpdate, SessionState } from "./types" + +function createStateStore(): { + store: SessionStateStore + resetCalls: string[] +} { + const state: SessionState = { + stagnationCount: 0, + consecutiveFailures: 0, + } + const resetCalls: string[] = [] + const progressUpdate: ContinuationProgressUpdate = { + previousStagnationCount: 0, + stagnationCount: 0, + hasProgressed: false, + progressSource: "none", + } + + return { + resetCalls, + store: { + getState: () => state, + getExistingState: () => state, + startPruneInterval: () => {}, + recordActivity: () => {}, + trackContinuationProgress: () => progressUpdate, + resetContinuationProgress: (sessionID: string) => { + resetCalls.push(sessionID) + }, + cancelCountdown: () => {}, + cleanup: () => {}, + cancelAllCountdowns: () => {}, + shutdown: () => {}, + }, + } +} + +describe("handleSessionIdle", () => { + it("resets continuation progress once when todos are empty", async () => { + // given + const sessionID = "ses_empty_todos" + const { store, resetCalls } = createStateStore() + const ctx = { + client: { + session: { + messages: async () => ({ data: [] }), + todo: async () => ({ data: [] }), + }, + }, + directory: "/tmp/test", + } + + // when + await handleSessionIdle({ + ctx: ctx as never, + sessionID, + sessionStateStore: store, + }) + + // then + expect(resetCalls).toEqual([sessionID]) + }) + + it("resets continuation progress once when every todo is complete", async () => { + // given + const sessionID = "ses_completed_todos" + const { store, resetCalls } = createStateStore() + const ctx = { + client: { + session: { + messages: async () => ({ data: [] }), + todo: async () => ({ + data: [ + { id: "todo-1", content: "Ship", status: "completed", priority: "high" }, + { id: "todo-2", content: "Verify", status: "completed", priority: "medium" }, + ], + }), + }, + }, + directory: "/tmp/test", + } + + // when + await handleSessionIdle({ + ctx: ctx as never, + sessionID, + sessionStateStore: store, + }) + + // then + expect(resetCalls).toEqual([sessionID]) + }) +}) diff --git a/src/hooks/todo-continuation-enforcer/idle-event.ts b/src/hooks/todo-continuation-enforcer/idle-event.ts index 162b60f6d..eebd83315 100644 --- a/src/hooks/todo-continuation-enforcer/idle-event.ts +++ b/src/hooks/todo-continuation-enforcer/idle-event.ts @@ -108,7 +108,6 @@ export async function handleSessionIdle(args: { } if (!todos || todos.length === 0) { - sessionStateStore.resetContinuationProgress(sessionID) sessionStateStore.resetContinuationProgress(sessionID) log(`[${HOOK_NAME}] No todos`, { sessionID }) return @@ -116,7 +115,6 @@ export async function handleSessionIdle(args: { const incompleteCount = getIncompleteCount(todos) if (incompleteCount === 0) { - sessionStateStore.resetContinuationProgress(sessionID) sessionStateStore.resetContinuationProgress(sessionID) log(`[${HOOK_NAME}] All todos complete`, { sessionID, total: todos.length }) return